Rust
cargo add agent-harness-adapter-coreRust 1.85 or newer. API reference on docs.rs.
Install an integration
Section titled “Install an integration”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).
Answer a hook
Section titled “Answer a hook”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.
The config, from Rust
Section titled “The config, from Rust”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.