HTTP API Reference
aoe serve exposes a small HTTP API so external orchestrators (other
agents, MCP tools, CI scripts) can drive sessions without attaching to
a terminal. This page documents the orchestration endpoints. The web
dashboard uses the same API surface plus additional internal routes.
Authentication
All endpoints require a token unless the server was started with
--no-auth. The token is the one printed by aoe serve (or visible
in the TUI’s Serve panel). Three transports are accepted:
| Transport | Example |
|---|---|
| Bearer header (recommended for clients) | Authorization: Bearer <token> |
| Query parameter | ?token=<token> |
| Cookie | aoe_token=<token> (set automatically by the dashboard) |
Read-only mode (aoe serve --read-only) blocks every write endpoint
with 403 read_only. Read endpoints work normally.
Skills
AoE discovers Agent Skills packages from its managed store and supported
user-level agent directories. A skill is a directory containing a valid
SKILL.md with name and description YAML frontmatter. External packages are
read-only. Adopt one to create an editable copy under AoE’s managed store.
Physical source roots are stable ids:
| Source id | Directory | Consumers |
|---|---|---|
claude-user |
~/.claude/skills |
Claude, OpenCode |
agents-standard |
~/.agents/skills |
Codex, OpenCode |
gemini-user |
~/.gemini/skills |
Gemini |
opencode-user |
~/.config/opencode/skills |
OpenCode |
kimi-legacy |
~/.kimi-code/skills |
Kimi legacy installations |
aoe-managed |
<app-dir>/skills |
AoE-managed packages |
GET /api/skills
Returns every discovered skill plus the external root registry. Skills with the same directory name remain separate source-qualified entries.
{
"skills": [
{
"directory": "review",
"name": "Review",
"description": "Review code carefully",
"provenance": { "kind": "external", "root": "claude-user" },
"provenanceLabel": "external:claude-user",
"writable": false
}
],
"roots": [
{
"id": "claude-user",
"label": "Claude",
"relativePath": ".claude/skills",
"consumers": ["claude", "opencode"],
"primaryAgent": "claude",
"legacy": false
}
]
}
GET /api/skills/{source}/{directory}
Reads one source-qualified package and returns its full SKILL.md in the
content field. Use aoe-managed for a managed skill or one of the external
source ids above.
POST /api/skills
Creates a managed skill and scaffolds its SKILL.md.
{ "directory": "release-check", "description": "Validate a release candidate" }
PUT /api/skills/{directory}
Replaces a managed skill’s SKILL.md after validating its frontmatter and
1 MiB size limit.
{ "content": "---\nname: release-check\ndescription: Validate a release candidate\n---\n" }
DELETE /api/skills/{directory}
Deletes a valid managed skill package. External packages cannot be deleted through AoE.
POST /api/skills/{source}/{directory}/adopt
Copies an external package into the managed store without changing the source.
The optional destination field changes the managed directory name.
{ "destination": "team-review" }
POST /api/skills/sync
Copies managed skills into the agents’ own skills directories, so a skill
authored once in AoE is available to every agent. Pass roots to limit the
sync; omit it to reach every root.
{ "roots": ["claude-user", "gemini-user"], "directories": ["review"], "replace": ["review"] }
directories narrows the sync to those skills, leaving every other one and
its copies alone; omit it to reconcile the whole store.
replace names skills AoE should take over, overwriting a skill it does not
manage or a propagated copy that was edited in place. It is the only way past
the never-overwrite rule below, so it must name each skill explicitly; an
omitted or empty replace overwrites nothing. A replaced entry becomes
AoE-owned, so later syncs keep it current on their own. Replacing a symlinked
entry moves the link aside and leaves whatever it pointed at alone, so a skill
managed by another tool keeps its own store.
Automatic syncs never replace anything.
Returns one outcome per skill per root rather than stopping at the first conflict.
{
"ok": true,
"outcomes": [
{ "root": "claude-user", "directory": "review", "status": "created", "message": null }
]
}
status is created, updated, unchanged, removed, conflict, or
error.
A propagated copy carries an .aoe-managed.json marker naming its root, its
skill, and the package digest at the time it was written. That marker is the
only thing that lets AoE later replace or remove the directory, and only while
the copy still matches the recorded digest. So a skill you wrote by hand, or a
propagated copy you have since edited, is reported as a conflict and left
exactly as it is; it is never overwritten, and it is never removed when its
managed source is deleted. A copy carrying a valid marker is listed once, as its
managed original, rather than twice.
This is also what makes AoE safe to run alongside a symlink-based skill manager
such as skillshare: a symlinked skill directory is something AoE did not
deploy, so it is reported and left in place rather than followed or replaced.
Setting skills.auto_propagate runs the same sync at session launch for the
agent being launched. It is off by default because it writes into your real
agent config directories.
All skill mutations require a read-write server and an elevated authenticated session when login is enabled. They are unavailable in CityHall mode. Adoption rejects symlinks, special files, packages over 64 MiB, individual files over 32 MiB, more than 1,024 files, and directory nesting deeper than 16 levels.
GET /api/sessions
List sessions. Returns every session by default, including trashed and
archived ones; pass state to filter server-side instead of fetching
everything and filtering client-side.
Query parameters
| Name | Default | Notes |
|---|---|---|
state |
(unfiltered) | live excludes trashed and archived sessions. trashed returns only trashed sessions. all (or omitting the param) is the historical unfiltered behavior. An unrecognized value is rejected with 400 rather than ignored, so a typo surfaces instead of silently returning every session. |
Example
curl -sS \
-H "Authorization: Bearer $AOE_TOKEN" \
"http://localhost:7777/api/sessions?state=live"
Status values
The status field on each session is PascalCase on the wire, and the same
spelling is used everywhere the HTTP API reports a status: GET /api/sessions,
the POST /api/sessions response (with or without ?wait=ready), and the
callback_url payload. Note this differs from the lowercase form the CLI and
[status_hooks] env vars use (AOE_NEW_STATUS=waiting), so a dispatcher
consuming both surfaces must not compare the two directly.
| Value | Meaning |
|---|---|
Starting |
Session was just created or restarted; the agent process is not yet up. |
Running |
Agent is actively working. |
Waiting |
Agent has stopped and is waiting for user input. This is the signal a dispatcher should treat as “needs a prompt”. |
Idle |
The agent’s turn has finished with no pending question. This is the signal a dispatcher should treat as “task complete”. |
Error |
The agent’s pane reported an error. |
Stopped |
The session’s tmux pane is gone (killed, exited, server restart). |
Unknown |
Status could not be determined. |
Deleting |
Session delete is in progress. |
Creating |
Session create is in progress, before Starting. |
POST /api/sessions
Create a session. The web dashboard uses this endpoint for the new-session dialog, and external orchestrators may call it directly.
Query parameters
| Name | Notes |
|---|---|
wait |
Set to ready to block the response until the new session’s status leaves Starting (or a 10s bound elapses), instead of returning immediately while the agent process is still coming up. The response status field reflects whatever the session actually reached, including Error if startup failed; a timeout does not mean success. |
Worktree fields
| Field | Notes |
|---|---|
worktree_enabled |
Set true to create a managed git worktree even when no explicit branch name is supplied. |
worktree_branch |
Optional explicit branch or worktree name. If omitted while worktree_enabled is true, AoE derives a safe branch name from the resolved session title. |
create_new_branch |
true creates a new branch; false attaches to an existing branch. |
For compatibility, callers that only send worktree_branch still opt into
worktree mode. To get title-derived branch names, send worktree_enabled as
true and omit worktree_branch.
Dispatcher fields
| Field | Notes |
|---|---|
callback_url |
An HTTP POST fires here when the session transitions to Waiting, Idle, or Error, so a dispatcher can react to completion without polling. Must be http/https and must not resolve to a loopback, private, or link-local address (rejected at create time, and re-resolved and re-checked before every dispatch; the approved address is then pinned for the request, so a DNS answer that changes in between cannot redirect it). Delivery is fire-and-forget: failures are logged server-side, not retried. The POST body is {"session_id", "old_status", "new_status", "at", "seq"}; seq is a per-process monotonic counter (resets on daemon restart) a dispatcher can use to discard an out-of-order delivery. |
idempotency_key |
A retry using the same key (even across a daemon restart, since the key is persisted on the created session) returns the existing session as 200 instead of creating a duplicate. Max 200 characters. If the originally-created session was later hard-deleted (not just trashed), the key is no longer found and a fresh session is created. |
callback_url is persisted with the session in the session store on disk, so
it survives a daemon restart. It is never echoed back in any API response, but
it is stored as given: prefer a URL that carries no credentials, and
authenticate deliveries by another means (for example a rotatable secret in the
path, or checking the session_id against the create you issued) rather than
embedding a bearer token in the query string.
Example
{
"path": "/path/to/repo",
"tool": "claude",
"title": "Fix Login Flow",
"worktree_enabled": true,
"create_new_branch": true
}
POST /api/sessions/{id}/send
Type a message into the agent and press Enter, the same way the TUI’s
send-message dialog and the aoe send CLI do. Honors the per-agent
paste-burst delay (e.g. Codex needs ~150 ms between text and Enter so
its burst-detection window expires before Enter arrives).
Request body (JSON)
{ "message": "review the diff and pick the smallest fix" }
message is sent literally. Newlines inside the string are sent as
shift-Enter (line break in the agent’s input box) and a final Enter
submits the whole message.
Responses
| Status | Body | When |
|---|---|---|
200 |
{"sent": true} |
Keys delivered to the tmux pane |
400 |
{"error": "message_empty"} |
message is empty or whitespace-only |
400 |
{"error": "acp_mode_unsupported"} |
Session is structured-view/ACP mode and has no tmux pane |
403 |
{"error": "read_only"} |
Server is in read-only mode |
404 |
{"error": "not_found"} |
No session with that id |
409 |
{"error": "session_not_running"} |
Session exists but the tmux pane is gone |
409 |
{"error": "resume_failed", "message": "...", "resume_session_id": "..."} |
Auto-revive tried to resume a stored conversation, but the pane exited before AoE could prove the ID invalid. The ID is preserved for explicit retry or replacement. |
409 |
{"error": "session_transient", "status": "..."} |
Session is mid-lifecycle and cannot accept input yet |
500 |
{"error": "tmux_error"} or {"error": "internal"} |
Unexpected failure (logged server-side) |
Concurrent POSTs to the same id are serialized server-side, so two
orchestrators racing on the same session won’t interleave keystrokes
inside the pane. Concurrent POSTs to different ids run in parallel.
Example
curl -sS -X POST \
-H "Authorization: Bearer $AOE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message":"summarize the failing test"}' \
"http://localhost:7777/api/sessions/abc123/send"
GET /api/sessions/{id}/output
Snapshot of the session’s tmux pane. Use this after send to read
what the agent printed back, or as a polling read-only view.
Query parameters
| Name | Default | Notes |
|---|---|---|
lines |
200 |
Number of trailing lines to capture. Clamped to 1..=2000. |
format |
text |
text strips ANSI escape sequences. ansi returns the raw pane bytes (use this if your client renders color). |
Responses
| Status | Body | When |
|---|---|---|
200 |
{"id": "...", "lines": N, "format": "text", "content": "..."} |
Pane captured |
400 |
{"error": "format_invalid", "allowed": ["text", "ansi"]} |
format was something other than text or ansi |
404 |
{"error": "not_found"} |
No session with that id |
409 |
{"error": "session_not_running"} |
Session exists but the tmux pane is gone |
500 |
{"error": "tmux_error"} or {"error": "internal"} |
Unexpected failure |
output does not require write access, so it works under
--read-only.
Example
curl -sS \
-H "Authorization: Bearer $AOE_TOKEN" \
"http://localhost:7777/api/sessions/abc123/output?lines=80&format=text"
Driving a session as a subagent
Together, send and output are the minimum primitive needed to run
an aoe session as a controlled subagent. A typical loop:
POST /api/sessions/{id}/sendwith the prompt.- Poll
GET /api/sessions/{id}/outputuntil the pane content stabilizes (no change between two reads spaced ~1 s apart) or the session list shows the session’sstatusback atIdle. - Capture the trailing region of
contentas the agent’s reply.
For long-running prompts, prefer polling status via
GET /api/sessions over polling output, then read output once
when status returns to Idle. Status transitions are also broadcast
to push subscribers if the dashboard’s push notifications are
configured.