Skip to content

Rust

Terminal window
cargo add agent-harness-adapter-core

Rust 1.85 or newer. API reference on docs.rs.

Implement Tool: your name, the harnesses, the Integration, and where the record of what was installed lives.

impl Tool for MyTool {
fn name(&self) -> &str {
"mytool"
}
fn harnesses(&self) -> Vec<Arc<dyn Harness>> {
harness::builtin()
}
fn integration(&self, _scope: Scope) -> Integration {
Integration::new()
.instructions("Run `mytool check` before you finish.\n")
.skill(Skill::new(
"mytool",
"Check the project with mytool.",
"Run `mytool check`; fix what it reports.\n",
))
.hook(Hook::new(Event::PreTool, "mytool hook {harness} {event}").tools(ToolKind::Shell))
.allow_command("mytool")
}
fn root(&self, _scope: Scope) -> Result<PathBuf> {
Ok(self.project.clone())
}
fn record_path(&self, _scope: Scope) -> Result<PathBuf> {
Ok(self.project.join(".mytool/harness.toml"))
}
fn declined_store(&self, _scope: Scope) -> Result<Box<dyn DeclinedStore + '_>> {
Ok(Box::new(TomlDeclined::new(
self.project.join(".mytool/config.toml"),
".mytool/config.toml",
)))
}
}

Then install (status and uninstall take the same arguments):

let tool = MyTool {
project: project.into(),
};
let opts = InstallOptions::new(["claude", "codex", "gemini"], Scope::Project);
print!("{}", install(&tool, &opts)?.to_text());

install returns an InstallResult: each harness’s parts with their states, notes and warnings, as text (to_text) or JSON (serde).

The hook command reads the harness’s input as a HookInput and answers with an Answer; emit writes it in the harness’s form.

/// `mytool hook <harness> <event>`: read the harness's input, answer in its form.
fn hook(harness: &str, event: &str) -> i32 {
let (Some(h), Ok(event)) = (harness::find(harness), event.parse::<Event>()) else {
// Exit 2 would block the agent: a hook that cannot work allows.
eprintln!("mytool: unknown harness or event: {harness} {event}");
return 0;
};
let mut stdin = String::new();
let _ = io::stdin().read_to_string(&mut stdin);
let input = HookInput::parse(h.as_ref(), event, &stdin).unwrap_or_default();
let command = input
.tool
.and_then(|t| claude_names::shell_command(&t.input))
.unwrap_or_default();
let answer = if command.contains("rm -rf") {
Answer::Deny {
reason: "mytool: no rm -rf here".into(),
}
} else {
Answer::Allow { stderr: None }
};
emit(
h.as_ref(),
event,
Ok::<_, String>(answer),
&mut io::stdout(),
&mut io::stderr(),
)
.map_or(1, i32::from)
}

hook::wire gives Claude Code’s input and reads Claude Code’s answers, for a command that speaks the contract itself.

manifest::Manifest reads a manifest or the config (manifest::find_config) and manifest::ManifestTool makes it a Tool, as the command does.

This page’s example is crates/lib/agent-harness-adapter-core/examples/mytool.rs; CI builds and runs it.