Adding a New Agent
Files touched
| File | Purpose |
|---|---|
src/agents.rs |
Registry entry (name, binary, detection, flags) |
src/tmux/detect/manifests/<agent>.toml |
Detection rules, for a pane-parsed agent |
src/tmux/status_detection.rs |
Detection entry point (manifest call or stub) |
src/hooks/mod.rs |
Hook installer, if the agent supports hooks |
src/session/instance/hooks.rs |
Hook wiring plus the AOE_INSTANCE_ID env prefix |
src/session/config/container_config.rs |
Config mount for the sandbox |
src/acp/agent_registry.rs |
ACP adapter entry, if the agent ships an ACP server |
src/acp/agent_profiles.rs, web/src/lib/agentProfiles.ts |
Structured view profile |
src/acp/install_hints.rs |
Install hint for aoe acp doctor and handshake failures |
docker/Dockerfile |
Install the agent in the sandbox image |
docs/structured-view.md, docs/index.md, README.md |
Supported-agent lists |
Levels of support
Each level is additive; do only what the agent supports.
| Level | What it gives | Requires |
|---|---|---|
| 1. Basic | Appears in aoe agents, sessions launch, status always Idle |
AgentDef plus a stub detect_status |
| 2. Pane-parse status | Status inferred from terminal output | A manifest plus detect_<agent>_status calling into it |
| 3. Hook status | The agent writes status the instant it changes | hook_config with the generic install_hooks(), or sidecar_hooks with a custom installer |
| 4. Session resume | A restart resumes the same native conversation | A session_support contract with verified resume argv |
| 5. Sandbox | Runs isolated with host config synced in | AgentConfigMount plus a Dockerfile install |
Levels 3 and 4 are independent. session_support declares verified native resume argv; its optional capture spec is the only backend allowed to supply an id, with each environment explicitly PaneScoped, Preassigned, ManagedExclusiveStore, or Unsupported. Use capture: None for argv-only support, and omit session_support entirely when the resume argv itself is unverified. Managed-store capture additionally requires a physically per-instance store, a cwd match, a launch-time floor, and a cross-process ownership lease.
Steps
1. Research the binary name, detection, YOLO flag, exact resume and fork argv, authoritative session-id source, host and sandbox storage paths, hook identity field, config dir, and install command. Treat an unverified environment as unsupported.
2. AgentDef in src/agents.rs: add to AGENTS, declaring detection, YOLO mode, hooks, lifecycle, and SessionSupport where resume argv is verified. Add a capture spec only for an authoritative source, naming one backend plus separate host and sandbox contexts. Hook-based capture must declare HookIdentityField::SessionId or ConversationIdOrSessionId from the upstream payload contract.
3. Status detection: an agent whose pane carries state gets a manifest in src/tmux/detect/manifests/<agent>.toml and a detect_<agent>_status calling into it (see detect_claude). Rules are {id, state, priority, region, matcher} and the highest-priority match wins, so a new case is a row rather than another branch. The hook file is a rule too (region = "hook"), which is what lets a blocking prompt on screen outrank a running write. Mark a rule visible = true only when it reads state off the agent’s own live chrome, which is what lets the poller publish it without a confirming capture. Agents with no pane signal keep a stub returning Status::Idle.
4. Hooks: for non-Claude formats add an installer in src/hooks/mod.rs (see install_hermes_hooks_with_events), wire it into SidecarHooks::install, and add the agent to status_hook_env_prefix() so AOE_INSTANCE_ID and AOE_PROFILE reach the hook. Without the instance id, hooks write nothing. Use HookStatus rather than raw strings, and keep installers pure file IO: any subprocess work belongs in a separate function so cargo test cannot mutate a developer’s real environment.
5. Container mount: add an AgentConfigMount (tool_name, host_rel, container_suffix, skip_entries) so the resolved config store is mounted where the containerized binary reads it, and install hooks and session-id sidecars into that mounted store. Declare a sandbox capture context only after this path is proven.
6. Dockerfile: install the agent and add its config dir to the mkdir -p block.
7. Structured view profile, if the agent ships an ACP server (its CLI accepts acp / --acp, or it ships a *-acp adapter): add the binary to agent_registry.rs::with_defaults() keyed on the src/agents.rs name, an install hint, a server profile registered in resolve(), and a mirrored frontend profile in PROFILES. Keep profiles conservative: until you have observed the adapter’s _meta convention for child tool calls, leave parent_meta_namespaces and the alias map empty, since missing indentation is safer than fake parent links and an empty alias map renders the correct generic card.
8. Tests: update the registry matrix and settings round-trip tests, then cover resume argv, the capture backend, host and sandbox contexts, missing-id fail-closed behavior, /clear rotation where supported, restart persistence, and two concurrent sessions in the same cwd. Managed-store tests must prove the launch floor and ownership lease reject stale or peer-owned ids, and hook agents that the declared identity field reaches the pane-scoped sidecar.
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
Hook format reference
Claude and Gemini (generic hook_config)
Set hook_config: Some(AgentHookConfig { ... }) and the generic install_hooks() handles their nested settings schema:
{
"hooks": {
"PreToolUse": [{"hooks": [{"type": "command", "command": "sh -c '...'"}]}],
"Stop": [{"hooks": [{"type": "command", "command": "sh -c '...'"}]}]
}
}
Each HookEvent carries:
| Field | Meaning |
|---|---|
name |
The agent’s event name, e.g. "PreToolUse". |
matcher |
Optional pattern, for events that need one. |
status |
Some(HookStatus::…) installs a status writer on this event; None is a purely lifecycle event. |
identity_field |
Installs a command that extracts the declared top-level native identity from stdin into the pane-scoped session_id sidecar. Use only a field documented upstream. With status also set, the identity command runs first, and it stays installed when agent_status_hooks = false. |
waiting_tools |
Tool names whose invocation blocks on the user for the tool’s whole execution (Claude’s AskUserQuestion). The status writer then inspects the payload’s tool_name and writes waiting instead of the event’s status. Pair it with a tool-scoped event that restores the normal status, or the status sticks on waiting. |
Other formats
- Cursor Agent: version 1
.cursor/hooks.json, with direct command entries underhooks.beforeSubmitPrompt. Its stable identity isconversation_id;generation_idis turn-scoped and must not be captured. UseSidecarHookswithinstall_cursor_hooks_with_events. - Codex: the same generic JSON payload, written to
hooks.jsonin Codex’s config dir (settings_rel_path: ".codex/hooks.json",format: HookFormat::CodexJson).install_codex_json_hooks()checks the adjacentconfig.tomlfeature opt-out first, and empty-event cleanup removes AoE’s entries while preserving user hooks. Sandbox installation refuses linked or unreadable config files rather than following them. Codex’sconfig.tomlholds[hooks.state]trust data and[features].hooks; do not pointsettings_rel_pathat it. - Hermes: custom YAML,
hooks.pre_tool_call[].command. - Kiro CLI: a custom JSON agent config with
hooks.preToolUse[].command. - Kimi Code: a flat
[[hooks]]array in.kimi-code/config.toml, which also holds provider and oauth settings, so the installer rewrites only its own entries.
Common pitfalls
- Missing
status_hook_env_prefix: withoutAOE_INSTANCE_IDhooks write nothing. Test by sending a message and checking/tmp/aoe-hooks-$(id -u)/*/statuson the host, or/tmp/aoe-hooks/*/statusinside the sandbox. - Sandbox hooks are separate: host installation skips containers, so wire into
build_container_configtoo. - Waiting status needs its own event: not every agent exposes an approval event. Document the gap and consider filing upstream.
- Sidebar quick permission response: the TUI’s
a/Aaction needs each agent’s exact keystroke sequence, not detection. SetAgentDef.permission_responseonly once you have confirmed by hand how the agent’s prompt is answered (bare digit, arrow plus Enter, no assumed trailing Enter). Setallow_always: Nonewhen the prompt offers no “don’t ask again” choice, and leave the whole fieldNoneuntil verified: the action then tells the user the agent is not supported yet.