Adding a New Agent

This guide walks through adding support for a new AI coding agent to AoE.

Overview

Adding an agent involves these files:

FilePurpose
src/agents.rsAgent registry entry (name, binary, detection, flags)
src/tmux/status_detection.rsStatus detection function (pane parsing or stub)
src/hooks/mod.rsHook installer (if the agent supports hooks)
src/session/instance.rsWire hook installation + env prefix
src/session/container_config.rsConfig mount for Docker sandbox
docker/DockerfileInstall agent in sandbox image
README.md, docs/Documentation updates

Levels of Support

Not all agents need everything. Here’s what each level provides:

Level 1: Basic (minimum viable)

  • Agent appears in aoe agents list
  • Sessions can be created and launched
  • Status always shows “Idle”

Requires: AgentDef entry + stub detect_status function.

Level 2: Status Detection via Pane Parsing

  • AoE reads the terminal output and infers running/waiting/idle
  • No agent-side configuration needed
  • Can be brittle if the agent’s UI changes

Requires: a detect_<agent>_status(content: &str) -> Status function that parses tmux pane content.

Examples: OpenCode, Codex, Vibe, Copilot, Pi, Droid.

Level 3: Status Detection via Hooks

  • AoE installs hooks into the agent’s config that write status to a file
  • Most reliable; survives UI changes
  • Requires the agent to support a hook/event system

Requires: either using the generic hook_config (if the agent uses the same JSON format as Claude/Cursor/Gemini) or a custom install_<agent>_hooks() function.

Examples: Claude, Cursor, Gemini (generic), Hermes (custom YAML), Kiro (custom JSON).

Level 4: Session Resume

  • AoE can restart a session and resume the prior conversation
  • Requires the agent to support a resume flag or session ID

Requires: setting resume_strategy in the AgentDef.

Level 5: Docker Sandbox

  • Agent runs in an isolated container
  • Config is synced from host to container

Requires: AgentConfigMount entry + Dockerfile installation.

Step-by-Step: Adding a New Agent

1. Research the Agent

Before writing code, gather:

  • Binary name: What command launches it? (e.g., kiro-cli, claude, codex)
  • Detection: How to check if it’s installed? Usually which <binary>.
  • YOLO/auto-approve flag: Does it have a flag to skip permission prompts?
  • Resume flag: Can it resume a prior session? What flag?
  • Hook support: Does it have lifecycle hooks (preToolUse, stop, etc.)?
  • Hook format: If hooks exist, what’s the config format? (JSON, YAML, TOML?)
  • Config directory: Where does it store config? (e.g., ~/.kiro, ~/.claude)
  • Install command: How do users install it?

2. Add the Agent Definition (src/agents.rs)

Add an entry to the AGENTS array:

AgentDef {
    name: "myagent",           // canonical name
    binary: "myagent-cli",     // binary to invoke
    aliases: &["my-agent"],    // alternative names for resolve_tool_name
    detection: DetectionMethod::Which("myagent-cli"),
    yolo: Some(YoloMode::CliFlag("--auto-approve")),
    instruction_flag: None,    // or Some("--system-prompt {}")
    set_default_command: false, // true if binary must be explicit in instance.command
    detect_status: status_detection::detect_myagent_status,
    container_env: &[],        // env vars for container sessions
    hook_config: None,         // or Some(AgentHookConfig { ... }) for Claude-format hooks
    resume_strategy: ResumeStrategy::Flag("--resume"),
    host_only: false,          // true if sandbox/worktree not supported
    send_keys_enter_delay_ms: 0,
    install_hint: "npm install -g myagent-cli",
},

3. Add Status Detection (src/tmux/status_detection.rs)

For hook-based agents, add a stub:

/// MyAgent status is detected via hooks, not tmux pane parsing.
pub fn detect_myagent_status(_content: &str) -> Status {
    Status::Idle
}

For pane-parsing agents, write a function that examines the terminal content:

pub fn detect_myagent_status(raw_content: &str) -> Status {
    let content = raw_content.to_lowercase();
    if content.contains("waiting for input") {
        Status::Waiting
    } else if content.contains("processing") {
        Status::Running
    } else {
        Status::Idle
    }
}

4. Add Hook Installation (if applicable)

If the agent supports hooks but uses a different format than Claude/Cursor/Gemini, add a custom installer in src/hooks/mod.rs. See install_hermes_hooks (YAML) or install_kiro_hooks (JSON) as examples.

Then wire it into install_agent_status_hooks() in src/session/instance.rs:

} else if self.tool == "myagent" && !self.is_sandboxed() {
    if let Some(home) = dirs::home_dir() {
        let config_path = home.join(".myagent/hooks.json");
        if let Err(e) = crate::hooks::install_myagent_hooks(&config_path) {
            tracing::warn!("Failed to install myagent hooks: {}", e);
        }
    }
}

And add the tool name to status_hook_env_prefix() so AOE_INSTANCE_ID is passed:

let has_hooks = agent.and_then(|a| a.hook_config.as_ref()).is_some()
    || tool == "settl"
    || tool == "hermes"
    || tool == "kiro"
    || tool == "myagent";

5. Add Container Config Mount (src/session/container_config.rs)

AgentConfigMount {
    tool_name: "myagent",
    host_rel: ".myagent",
    container_suffix: ".myagent",
    skip_entries: &["sandbox", "sessions", "cache"],
    seed_files: &[],
    copy_dirs: &[],
    keychain_credential: None,
    home_seed_files: &[],
    preserve_files: &[],
    clean_files: &[],
},

6. Add to Dockerfile (docker/Dockerfile)

# Install MyAgent
RUN curl -fsSL https://myagent.dev/install | bash

And add the config directory to the mkdir -p block.

7. Update Tests

In src/agents.rs, update:

  • test_get_agent_known
  • test_agent_names
  • test_resolve_tool_name
  • test_settings_index_roundtrip
  • test_send_keys_enter_delay
  • test_install_hint_lookup

In src/tmux/status_detection.rs, add a test for your detection function.

In src/session/instance.rs, add your agent to test_status_hook_env_prefix_includes_hermes (if hook-based).

8. Update Documentation

  • README.md: features list + FAQ
  • docs/index.md: supported agents list
  • docs/guides/sandbox.md: sandbox overview + image table
  • docker/Dockerfile.dev: comment listing inherited agents

9. Verify

cargo fmt
cargo clippy -- -D warnings
cargo test --lib agents
cargo test --lib <youragent>
cargo test --lib container_config
cargo build
./target/debug/aoe agents  # verify detection works

Hook Format Reference

Claude/Cursor/Gemini (generic hook_config)

{
  "hooks": {
    "PreToolUse": [{"hooks": [{"type": "command", "command": "sh -c '...'"}]}],
    "Stop": [{"hooks": [{"type": "command", "command": "sh -c '...'"}]}]
  }
}

Set hook_config: Some(AgentHookConfig { ... }) in the agent def; the generic install_hooks() handles it.

Hermes (custom YAML)

hooks:
  pre_tool_call:
    - command: "sh -c '...'"

Kiro CLI (custom JSON agent config)

{
  "name": "aoe-hooks",
  "tools": ["*"],
  "hooks": {
    "preToolUse": [{"command": "sh -c '...'"}],
    "stop": [{"command": "sh -c '...'"}]
  }
}

Common Pitfalls

  • Forgetting status_hook_env_prefix: Without AOE_INSTANCE_ID, hooks write nothing. Add your tool name to the check in src/session/instance.rs.
  • Wrong hook format: Each agent has its own schema; test that hooks actually fire by sending a message and checking /tmp/aoe-hooks/*/status.
  • Sandbox hooks are separate: Host hook installation (install_agent_status_hooks) doesn’t cover sandbox sessions. You also need to wire hooks into build_container_config in src/session/container_config.rs so the sidecar volume is mounted and the agent config is materialized inside the container.
  • Don’t shell out in install_*_hooks(): Keep hook installation as pure file IO. Any subprocess calls (like setting a default agent) should be in a separate function so cargo test doesn’t mutate the developer’s real environment.
  • Use structured output when parsing agent CLIs: If the agent CLI has --format json or similar, use it instead of substring matching on human-readable output. Human-readable formats change between versions.
  • Waiting status needs a dedicated event: Not all agents have an approval/permission event. If the agent doesn’t expose one, document it as a limitation and consider filing upstream.
  • Redundant Dockerfile ENV: Check if PATH is already set before adding another layer.
  • set_default_command: Only needed for agents where the binary name alone isn’t sufficient (e.g., opencode needs explicit command storage).