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:
| File | Purpose |
|---|---|
src/agents.rs | Agent registry entry (name, binary, detection, flags) |
src/tmux/status_detection.rs | Status detection function (pane parsing or stub) |
src/hooks/mod.rs | Hook installer (if the agent supports hooks) |
src/session/instance.rs | Wire hook installation + env prefix |
src/session/container_config.rs | Config mount for Docker sandbox |
docker/Dockerfile | Install 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 agentslist - 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_knowntest_agent_namestest_resolve_tool_nametest_settings_index_roundtriptest_send_keys_enter_delaytest_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 + FAQdocs/index.md: supported agents listdocs/guides/sandbox.md: sandbox overview + image tabledocker/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: WithoutAOE_INSTANCE_ID, hooks write nothing. Add your tool name to the check insrc/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 intobuild_container_configinsrc/session/container_config.rsso 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 socargo testdoesn’t mutate the developer’s real environment. - Use structured output when parsing agent CLIs: If the agent CLI has
--format jsonor 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).