⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⢃⣤⡶⠂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣿⣶⣤⡀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠄⠀⠀⠀⠀⠀⢀⣠⣶⣿⣥⣾⣿⣿⣿⣿⣿⣍⠀⠀⠀⠀⠀⠀⠀⠀⠀⣄⡀
⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣯⣴⡾⠋⠀⠀⠀⠸⡿⠟⠋⣹⣿⡿⢿⣿⣿⣿⣿⢧⠀⠀⠀⠀⠀⠙⢷⣦⣼⣿⣄
⠀⠀⠀⠀⠀⠀⣰⡾⠟⣿⣿⣿⣿⣿⣶⠄⠀⠀⢀⣠⢾⡿⠉⠀⢨⣿⣿⣿⣿⡄⠀⠀⠀⠰⣶⣿⣿⣿⣿⣿⠿⣷⣄
⠀⠀⠀⠀⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠈⠃⠀⠀⠀⠀⣼⣿⣿⣿⡏⠁⠀⠀⣴⣾⣿⣿⣿⣿⣿⣿⣷⣾⣿⣷⣦
⠀⠀⠀⠀⠙⠋⠀⣠⣿⠿⠛⠉⢻⣿⣿⣿⣧⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⡿⠁⠀⠀⢠⣾⣿⣿⣿⠟⠉⠻⢿⣷⡀⠘⠹⠋
⠀⠀⠀⠀⠠⢴⠋⠻⠇⠀⠀⠀⠀⣿⣿⣿⣿⠃⠀⠀⠀⢀⣾⣿⣿⣿⣿⠃⠀⠀⠀⢻⣿⣿⣿⡏⠀⠀⠀⠀⠿⠟⠳⡤
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⡆⠀⠀⠀⣼⣿⣿⣿⣿⡇⠀⠀⠀⢀⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠁
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣄⠀⢰⣿⣿⣿⣿⣿⡇⠀⠀⣠⣿⣿⣿⣿⡟
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣷⣼⣿⣿⣿⣿⣿⣷⣴⣾⣿⣿⣿⣿⡟
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡋⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠃
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠐⢻⣿⠿
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠘⣿⡄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣁⣀⠀⠀⣿⣿⡄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⡏⠙⠻⠿⣿⣿⣿⣿⢿⣿⣿⣿⡿⣿⣿⣿⣿⣿⣿⣧⣤⣾⣿⣿⠃
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⡿⠁⠀⠀⢀⣿⣿⣿⠃⠸⣿⣿⣿⠃⠈⢻⣿⣿⡿⢿⣿⣿⡿⠟⠃
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣾⣿⣿⡿⠀⠀⢠⢶⣾⣿⡿⠃⣠⣴⣿⣿⣿⠀⢠⣶⣿⣿⣷
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠉⠛⠙⠋⠉⠁⠀⠀⠀⠀⠀⠁⠀⠀⠋⠙⠉⠟⠉⠀⠈⠈⠁⠉
Status: experimental. A multi-client session daemon for the Agent Client Protocol (ACP). Many heads, one body, many feet: multiple clients (editors, dashboards, Slack bridges) attach to one daemon that manages the real ACP agent processes underneath. Every attached client sees the same live session in real time.
hydra-acp is a session manager, multiplexer, and TUI for AI coding agents. One daemon manages your agent processes and the sessions running inside them; many clients — a terminal TUI, your editor, a browser, Slack — attach to the same live session at once and see it update in real time. Start a session at your desk, follow it from your phone, hand it off to a teammate.
npm install -g @hydra-acp/cli
hydra-acp # launch the TUI and start a sessionThat's the whole getting-started path: install, run, talk to an agent. The TUI is the front door — it picks an agent, drives the conversation, lists your sessions, and lets you attach to one that's already running. From there you can wire up your editor, add a browser or Slack bridge, or pipe data through it on the command line.
- A session manager. Every session is tracked, named, listable (
hydra-acp session), exportable, and resurrectable. Close your terminal and the session keeps running in the daemon; reattach later from anywhere. - A multiplexer. Many clients share one live session. Events broadcast to all attached clients; prompts serialize through a per-session queue; permission requests race (first response wins). Editor, TUI, browser, and Slack all watch the same agent at once.
- A TUI. A full terminal UI for driving sessions interactively — picking agents, prompting, approving tool calls, scrolling transcripts, and switching between live sessions.
- Extensions. Optional companion processes — Slack bridge, web UI, desktop notifier, auto-approver, cross-machine sync — that the daemon spawns and manages for you. See Extensions below.
Under the hood, hydra-acp is a daemon + CLI shim that implements two open ACP RFDs as a single coherent surface, on top of the standard ACP protocol (including session/list for session discovery), plus the official ACP Registry as its agent-distribution mechanism. The rest of this section is the protocol detail; skip to Quick start if you just want to use it.
ACP itself is the Agent Client Protocol — a JSON-RPC 2.0 protocol between editors (clients) and AI coding agents. Today the protocol is canonically a 1:1 stdio relationship: one editor spawns one agent and owns its stdin/stdout. Two RFDs in the agentclientprotocol/agent-client-protocol repo extend that model. hydra-acp is one daemon that implements both together so they can be used as a coherent system rather than two independent extensions.
1. Multi-Client Session Attach — RFD #533
Adds two new methods that turn ACP from 1:1 into 1:N:
session/attach { sessionId, historyPolicy, clientInfo? }— a second (or third, or N-th) client connects to a session that's already live.historyPolicycontrols replay on attach:"full","pending_only", or"none".session/detach { sessionId }— graceful disconnect; the session continues as long as one client remains attached.
Every event the agent emits is broadcast to every attached client; clients self-filter what they act on. Permission requests broadcast the same way: the first response wins, and the rest receive a session/update notification with sessionUpdate: "permission_resolved". Capability is advertised in initialize under agentCapabilities.sessionCapabilities.attach.
2. Streamable HTTP & WebSocket Transport — RFD: streamable-http-websocket-transport (WebSocket profile only)
Defines the network transport that lets ACP run between processes that aren't parent and child. The RFD specifies two profiles on one /acp endpoint: a Streamable HTTP profile (POST/GET-SSE/DELETE with Acp-Connection-Id and Acp-Session-Id headers, HTTP/2 required) and a WebSocket profile (GET with Upgrade: websocket). The RFD explicitly permits servers to support only the WebSocket profile, and that's the route hydra-acp takes — the Streamable HTTP half isn't implemented. The RFD itself is still Draft as of April 2026, with the routing model rewritten twice in the six weeks before this writing, so deferring HTTP-transport work until the spec stabilizes is deliberate.
On the WebSocket side, hydra-acp exposes its WSS endpoint at /acp: a client sends GET /acp with Upgrade: websocket, receives a 101 Switching Protocols response, and the connection becomes a bidirectional stream of JSON-RPC text frames (binary frames are ignored). The server negotiates the acp.v1 subprotocol via the standard Sec-WebSocket-Protocol mechanism (echoed back in the 101 when advertised; absent otherwise). Authentication is layered on top — HTTP headers, query parameters, or WebSocket subprotocols — and is treated as orthogonal by the spec. hydra-acp authenticates via a bearer token carried in a hydra-acp-token.<token> subprotocol entry or a ?token=<token> query parameter.
Beyond the bedrock of initialize / session/new / session/prompt, the daemon implements session/list (Protocol: Session List, stabilized 2026-03-09) so any compliant client can enumerate sessions known to the daemon and attach to one — { sessionId, cwd, title?, updatedAt?, _meta? } per entry, with cwd filtering and cursor-based pagination. Hydra-specific fields ride under _meta["hydra-acp"] per the Extensibility convention.
Agents are sourced from the ACP Registry — a CDN-hosted JSON document at https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json. Each entry declares id, name, version, description, and a distribution block that selects between npx, binary, or uvx installation. hydra-acp caches the registry locally with a 24-hour TTL, falls back to the cached copy on network failure, and resolves an agent's distribution to a spawn plan when a session needs that agent.
editor browser Slack ← clients
│ │ │
hydra-acp hydra-acp-browser hydra-acp-slack ← hydra extensions
│ │ │
└───────────────┼───────────────┘
│
WSS / HTTP
│
hydra-acp ← hydra daemon
│
T1 → T2 → … → Tn ← hydra transformers
│
┌───────────────┼───────────────┐
│ │ │
claude opencode gemini ← agents
- Editor spawns
hydra-acpas it would any ACP agent. The shim looks like a normal stdio agent. - Shim opens a WSS connection to the daemon at
/acp, authenticating via the bearer token. session/newfrom the editor → daemon resolves the requested agent against the cached ACP Registry, downloads it on first use under~/.hydra-acp/agents/, spawns it as a child process, and creates an ACP session inside it.session/attachfrom a second client → daemon adds the new client to the session's broadcast list and replays history perhistoryPolicy(per RFD #533).- Notifications fan out to every attached client. Prompts are serialized through the daemon's per-session queue. Permission requests broadcast to every attached client; first response wins and the rest receive a
session/updatewithsessionUpdate: "permission_resolved"carrying the resolving client's outcome. session/listreturns the daemon's sessions (live and cold), filterable bycwd.session/detachlets a client leave voluntarily; the session continues until the last client detaches (per RFD #533).
Existing ACP clients are stdio-based: they spawn(command) a process and exchange JSON-RPC over its stdin/stdout. A shim that looks like an ACP agent on stdio is zero-integration on the client side — the client doesn't need to know anything about hydra, the daemon, or WSS. It just spawns hydra-acp and starts talking ACP.
Clients that adopt the streamable-http-websocket-transport RFD natively can connect to the daemon's /acp endpoint directly without the shim.
hydra-acp cat is a pipe-friendly headless verb: it feeds stdin to a fresh
session as the user prompt and streams the agent's text reply to stdout. No
TUI, no JSON-RPC for the caller, no terminal control sequences in the
output — just text in, text out, exit code 0 on a clean turn. Hydra ends up
usable as a unix filter, with the agent as the program in the middle of the
pipeline.
A few properties keep it well-behaved:
- Sandboxed cwd by default. Piped invocations get a fresh empty tempdir as
the agent's
cwd, and the permission handler rejects every tool call that isn't one of thehydra-acp-stdinMCP tools (head / tail / grep / read on the piped bytes). The agent has nothing to look at except the data you piped in. Override with--cwd <path>when you want it poking at the project (e.g. "find docs in the codebase that mention this error"). - Smart about size. Small inputs are inlined into the prompt. Large inputs
(default >1 MiB) get the daemon's in-memory
hydra-acp-stdinMCP server: bytes flow into a ring buffer and the agent pulls them on demand viahead,tail,grep,read, andinfo. A multi-gigabyte log isn't a context-window problem; it's a fixed-size buffer the agent samples. --followfor live streams. Pipetail -finto--followand each quiet burst on stdin is sent as a new turn. The standing prompt (-p) is sent only on the first turn; later turns carry just the new bytes.--detachto share the session. By default the session lives as long as the cat process; on stdin EOF it dies. With--detachit stays in the daemon,hydra-acp sessionlists it, and the slack / browser / notifier extensions can ride on it. Useful for kicking off a long-running watch from a shell script and following it on your phone.
A few examples:
# One-shot question, no stdin.
hydra-acp -p "tools to convert a HEIC photo to JPEG on linux?"
# Analyze a big log without copy-pasting it into a chat window.
journalctl -u nginx --since "1 hour ago" | hydra-acp cat -p "anything alarming?"
# Treat hydra as the filter in a unix pipeline — output is plain text,
# so tee / grep / jq downstream just work.
git log --since="last monday" --pretty=full | hydra-acp cat -p "draft release notes, one bullet per user-visible change, grouped by version" | tee RELEASE_NOTES.md
# Watch a live log and only speak up when something's wrong. --detach
# keeps the session in the daemon, so you can follow it on your phone
# via the slack extension after closing the shell.
tail -F /var/log/app.log | hydra-acp cat --follow --detach -p "if a line looks like an error or stack trace, summarize it. otherwise stay silent."Sessions created by cat are normal hydra sessions, so hydra-acp session,
session export, /hydra title, and the rest of the surface all work on them.
npm install -g @hydra-acp/cliDrops hydra-acp (and hydra) on your PATH.
Then pick the agent the daemon should spawn by default. defaultAgent is the registry id used when session/new doesn't specify one (the common case for editor-spawned shims), and defaultModels[<agent>] pins a per-agent default model. Both are read once at daemon startup, so a daemon restart is needed for changes to take effect:
hydra-acp agent list # browse known agent ids
hydra-acp agent set opencode # update the default agent
hydra-acp agent set opencode openai/gpt-5-codex # update the default model for agent
hydra-acp daemon restart # restart the daemon to pickup changes# 1. (Optional) Initialize: writes ~/.hydra-acp/config.json with a generated
# bearer token. If you skip this, the first invocation of `daemon start`,
# `shim`, or `tui` writes the config for you. Run init explicitly only
# when you want to rotate the token (`hydra-acp init --rotate-token`).
hydra-acp init
# 2. (Optional) Start the daemon. If you skip this step, the shim will
# auto-start the daemon the first time an editor invokes it.
hydra-acp daemon start
# 3. Configure your editor to spawn `hydra-acp shim` instead of an agent
# directly. The `shim` verb forces shim mode — the right form for
# spawned-by-editor cases where stdio is already piped. The first
# session/new asks the daemon which agent to spawn (defaults to
# config.defaultAgent). If you'd rather the editor pin a specific agent,
# spawn `hydra-acp launch <agent>` (see "Launcher mode" below).
# 4. From a terminal, drive a session interactively (TUI).
hydra-acp # bare invocation in a TTY launches the TUI
hydra-acp tui # explicit form
# 5. List live sessions.
hydra-acp session
# 6. Attach a second client to an existing session.
# Bare invocation auto-detects: TUI in a terminal, ACP shim when piped.
hydra-acp --session hydra_session_abc123Hydra can spawn user-configured extension processes when the daemon starts. Extensions are arbitrary commands — written in any language — that talk to the daemon over its existing REST or WSS endpoints. Hydra handles their lifecycle (spawn on start, kill on stop, auto-restart on crash with exponential backoff up to ~60s) and injects daemon connection info via env vars.
Various ready-made extensions ship under the same @hydra-acp npm scope. All are optional and can be installed independently.
@hydra-acp/slack — Slack thread bridge. Each hydra session gets its own Slack thread; the agent's prose, tool cards, plans, and permission prompts stream in, and replies typed in the thread come back to the agent as user prompts. Useful for non-developer collaborators, or for driving an agent from your phone while you're away from the keyboard. Respects RFD #533's prompt_received and survives daemon restarts via session resurrection.
npm install -g @hydra-acp/slack
hydra-acp extension add hydra-acp-slack
hydra-acp extension restart hydra-acp-slackYou'll also need a Slack app and a config at ~/.hydra-acp-slack.conf — see the package's setup section for scopes, tokens, and authorized users.
@hydra-acp/browser — local web UI. Single-page app that lists live sessions, attaches to each one, and renders the transcript (agent messages, tool calls, plans, mode/model changes) with a composer for prompting and permission widgets for approving tool use. Cheap to bring up when you want to spot-check an agent without firing up the editor.
npm install -g @hydra-acp/browser
hydra-acp extension add hydra-acp-browser
hydra-acp extension restart hydra-acp-browserThe first launch generates ~/.hydra-acp-browser/authkey and writes the open URL (with ?authkey=…) to ~/.hydra-acp-browser/link. Defaults to localhost-only; see the package's HTTPS section for binding to a LAN address with TLS.
@hydra-acp/notifier — desktop notifications. Always-on companion that fires notify-send (Linux) or osascript (macOS) when sessions emit notable events — by default, turn_complete. The default title is 🐉 <agentId> · <short-session-id> · <session-title-or-cwd> and the body renders the agent's stop reason as friendly text (Finished, Max token limit reached, etc.). Drop a JS rule at ~/.hydra-acp/notifier.config.js to customize per-event, or set HYDRA_ACP_NOTIFY_CMD to route everything to ntfy/Pushover/your phone.
npm install -g @hydra-acp/notifier
hydra-acp extension add hydra-acp-notifier
hydra-acp extension start hydra-acp-notifier@hydra-acp/approver — headless permission auto-responder. Attaches to every live session and answers session/request_permission based on a JS rule at ~/.hydra-acp/approver.config.js. When the rule returns an optionId it wins the race and dismisses the prompt before any human client sees it; when it abstains (returns null), the prompt stays open for your interactive clients. Useful for centralizing approval policy in one place so per-client approval can go away.
npm install -g @hydra-acp/approver
hydra-acp extension add hydra-acp-approver
hydra-acp extension start hydra-acp-approverWithout a config file the approver abstains on everything — installing it has no behavioral effect until you write a rule.
@hydra-acp/archiver — cross-machine session sync. Uploads session bundles to a shared backend (Google Drive, plain filesystem) after every turn and imports peers' bundles in the background, so a session started on machine A shows up on machine B without manual export/import. Imported sessions carry an importedFromMachine breadcrumb that the picker, browser, slack, and sessions list honor for host filtering.
npm install -g @hydra-acp/archiver
hydra-acp extension add hydra-acp-archiver
hydra-acp extension start hydra-acp-archiverSee the package README for backend setup (Drive OAuth, filesystem path).
@hydra-acp/planner — multi-agent project orchestrator. Invoked via /hydra planner create <description> from any session: asks the host agent to decompose the project into a task DAG, then spawns N worker sessions and drives them in parallel by prompt management, with progress streaming back into your original chat. Boards persist under ~/.hydra-acp/planner/projects/<id>/ so plans survive daemon restarts. Strictly speaking it's a transformer, not an extension — it sits in the daemon's message pipeline rather than attaching as a client — but it installs and configures the same way.
npm install -g @hydra-acp/planner
hydra-acp transformer add hydra-acp-planner
hydra-acp transformer start hydra-acp-plannerSee the package README for the full set of /hydra planner verbs (status, retry, cancel, …) and the task-DAG format.
Configure in ~/.hydra-acp/config.json:
{
"extensions": {
"hydra-acp-slack": {},
"hydra-acp-browser": {
"command": ["hydra-acp-browser"],
"args": ["--port", "9999"],
"env": { "UI_THEME": "dark" }
}
}
}If command is omitted, it defaults to [<name>] — useful when the package's bin matches its key (e.g. npm install -g @hydra-acp/slack exposes hydra-acp-slack on PATH, so "hydra-acp-slack": {} is enough).
Each extension is launched with these env vars set:
| Env var | Example |
|---|---|
HYDRA_ACP_DAEMON_URL |
http://127.0.0.1:55514 |
HYDRA_ACP_DAEMON_HOST |
127.0.0.1 |
HYDRA_ACP_DAEMON_PORT |
55514 |
HYDRA_ACP_TOKEN |
hydra_token_<hex> |
HYDRA_ACP_WS_URL |
ws://127.0.0.1:55514/acp |
HYDRA_ACP_HOME |
~/.hydra-acp |
HYDRA_ACP_EXTENSION_NAME |
the name from config |
Extension stdout/stderr are appended to ~/.hydra-acp/extensions/<name>.log.
While the daemon is running you can manage extensions without bouncing it:
hydra-acp extension list
hydra-acp extension restart hydra-acp-slack
hydra-acp extension log hydra-acp-slack --follow
stop suppresses the auto-restart backoff; the extension stays down until the next start, restart, or daemon bounce. add/remove are config-only — restart the daemon to apply. Per-extension config (env vars, args, custom command paths) goes in the same extensions block. hydra-acp extension log <name> -f tails an extension's stdout/stderr if you need to debug.
Trust model: each extension receives its own per-process token scoped to that process's lifetime. The token grants the same read/write access to the daemon's REST and WSS surfaces as a logged-in client. Treat extensions as part of your trusted compute base — review extensions before installing and don't run untrusted code through this mechanism. See cli/examples/client-observe.mjs for an annotated reference implementation.
hydra-acp # auto-dispatch: TUI in a TTY, shim when stdio is piped
hydra-acp shim # explicit shim mode (forces shim regardless of TTY)
hydra-acp tui # explicit terminal-UI mode
hydra-acp launch <agent> # launcher mode: shim that forces the
# daemon to spawn <agent> on session/new
hydra-acp cat [-p <prompt>] [--detach] # pipe-friendly headless mode: feeds stdin
# to a session as prompts and streams the
# agent's reply to stdout
hydra-acp --session <id-or-url> # attach to existing session
# (TUI in a TTY, shim otherwise)
hydra-acp --reattach # pick the most-recent session for cwd
hydra-acp --new # force a fresh session
hydra-acp --readonly # open a session as a transcript viewer (with --session)
hydra-acp init # generate the service token
hydra-acp daemon [status] # output status of daemon
hydra-acp daemon start [--foreground] # detached by default; --foreground to attach
hydra-acp daemon stop # stop running daemon
hydra-acp daemon restart # stop then start the daemon
hydra-acp daemon log [-f] [-n N] # tail (default 50) or follow the daemon log
hydra-acp session [list] # list sessions
hydra-acp session info <id> [--verbose] [--json] [--diff] [--fold] [--no-color] [--no-pager]
# aggregate one session: turn count, tool histogram,
# files touched, cost/duration, synopsis.
# --diff appends the session diff under the summary
# and pages the whole thing on a TTY; honors --fold.
hydra-acp session diff <id> [--json] [--no-color] [--no-pager] [--fold]
# git-diff-shaped view of every file the session
# edited, reconstructed from history (no git, no fs).
# Pages through $HYDRA_ACP_PAGER → $PAGER → less on a TTY
# (LESS=FRX default); --no-pager bypasses.
# --fold collapses sequential hunks that rewrite the
# same region (agent thrash) into one net-effect hunk.
hydra-acp session kill <id> # close a live session (keeps the on-disk record so it can be resurrected)
hydra-acp session remove <id> # remove a session entirely (live or cold)
hydra-acp session export <id> [--out <file>|.]
# write a session bundle (meta + history) to <file>,
# to a default-named file when --out=., or to stdout
hydra-acp session transcript <id>|<file> [--out <file>|.]
# render a session (id via daemon, or a local .hydra
# bundle) as a markdown transcript
hydra-acp session import <file>|- [--replace] [--cwd <path>] [--info]
# import a bundle from <file> or stdin (-);
# --replace overwrites a lineage match (kills it
# if live); --cwd overrides the bundle's recorded
# working directory; --info prints the bundle's
# meta without importing
hydra-acp extension [list] # list configured extensions and live state
hydra-acp extension add <name> # add to config (--command, --args, --env, --disabled)
hydra-acp extension remove <name> # remove from config
hydra-acp extension start|stop|restart <n> # lifecycle on a running extension
hydra-acp extension log <name> [-f] [-n] # tail (default 50) or follow an extension's log
hydra-acp transformer [list] # list configured transformers and live state
hydra-acp transformer add <name> # add to config (--command, --args, --env, --enabled; disabled by default)
hydra-acp transformer remove <name> # remove from config
hydra-acp transformer start|stop|restart <n> # lifecycle on a running transformer
hydra-acp transformer log <name> [-f] [-n] # tail (default 50) or follow a transformer's log
hydra-acp agent [list] # list agents in the registry
hydra-acp agent install <id> # pre-install an agent (else lazy on first use)
hydra-acp agent set [<id>] [model] # with no args, report the daemon's current default
# agent and its default model. With <id>, set <id> as
# the default agent (config.defaultAgent). With <id>
# and [model], set the per-agent default model
# (config.defaultModels[<id>]). Writes require
# `daemon restart` to take effect.
hydra-acp agent refresh # force a registry re-fetch
hydra-acp agent sync <id> # spawn <id> just long enough to ACP session/list it,
# then persist any sessions it remembers as cold rows
# (lets you bring in pre-existing agent sessions)
hydra-acp auth # list active session tokens
hydra-acp auth password [--force] # set the daemon's master password
hydra-acp auth revoke <id> # revoke a session token
A bare invocation (hydra-acp with no subcommand) auto-dispatches based on whether stdout is a TTY: a real terminal launches the TUI, a piped stdio (the editor-spawned case) drops into shim mode. Pass shim or tui explicitly to force one or the other. Editors should configure hydra-acp shim so the choice is unambiguous regardless of how the editor wires stdio.
hydra-acp launch <agent> is a convenience for "shim me, and use this registry agent." It's the easiest way to wrap an existing ACP-speaking editor configuration whose agent-spawn surface is just a command and arguments:
# Configure your editor's ACP-launch command to:
hydra-acp launch claude-code
When the editor sends session/new, the shim injects the agent id under _meta["hydra-acp"].agentId (e.g. "claude-code") before forwarding to the daemon — the spec session/new params stay clean. The daemon resolves claude-code against the cached ACP Registry, downloads/installs the agent on first use under ~/.hydra-acp/agents/, and spawns the subprocess. The editor sees a normal ACP agent. From then on, hydra-acp session lists the live session and any other client can session/attach to it.
<agent> is the registry ID — e.g. claude-code, gemini-cli, codex. Run hydra-acp agent to browse what's available, or fetch the registry CDN URL directly.
If both launch <agent> and --session-id are given, --session-id wins (attach mode); the agent is ignored because the agent process is already running.
Pass --name <label> or set HYDRA_ACP_NAME and the first session/new from that shim is labeled accordingly. The label flows through _meta["hydra-acp"].title on the wire, lands in Session.title, and shows up in session/list and hydra-acp session. Subsequent session/new calls from the same shim are not labeled — first one wins. The label survives daemon restart (it's carried in the resume hints).
HYDRA_ACP_NAME="$BUFFER_NAME" hydra-acp launch claude-acp
# or
hydra-acp --name "$BUFFER_NAME" launch claude-acp
After the first user prompt lands, hydra automatically replaces the label with the first line of that prompt (truncated, ≤80 chars) and emits a session_info_update so every attached client (TUI, slack, browser) refreshes its header. Agents that emit their own session_info_update override that — last write wins.
Sometimes you want to scroll through a session's transcript — usually one imported from another machine — without spawning the underlying agent. Pass --readonly to tui to attach in view-only mode:
hydra-acp tui --resume <id> --readonly
The daemon enforces the contract: a read-only attach to a cold session takes a viewer path that streams history straight from disk — no manager.resurrect, no agent process. Any mutating method sent from a read-only connection is refused. History replay and live updates are unchanged, so the existing scrollback search (^R when scrolled back) works over the full transcript. (Wire details — including how the read-only flag is carried — live in PROTOCOL.md.)
The TUI suppresses the composer entirely — those rows go to scrollback so you see more of the conversation. The window title is suffixed [VIEW ONLY] so the mode is unambiguous. Prompt-shaped keys (Enter, Shift+Enter, Shift+Tab) are inert; ^P, ^G, ^L, ^R, PgUp/PgDn, ^C, ^D work as usual.
From inside the TUI's session picker, v on a selected row enters view-only mode for that session. Enter still attaches normally. The mode is per-session: ^P → pick another with Enter drops out of read-only; v re-enters it.
Slash commands of the form /hydra <verb> [args] are intercepted by hydra before the prompt reaches the agent. They never appear in the conversation log; the only client-visible signal is the notification(s) the verb implies.
| Command | Effect |
|---|---|
/hydra title |
Asks the agent for a one-line summary, applies it as the new title via session_info_update. The sub-prompt and reply are suppressed from clients. |
/hydra title <text> |
Sets the title to <text> directly. No agent call. |
/hydra agent <agent> |
Swaps the agent process backing this session. Spawns the new agent (must be in the registry — see hydra-acp agent list), kills the old one, and feeds the conversation transcript so far back in as the first prompt to the new agent. session_info_update carries the new agentId; a synthetic agent_message_chunk banner marks the switch in the transcript. The on-disk session record is updated so resurrection brings the session back on the new agent. |
These work from anywhere a session prompt can be typed — the TUI's input box, agent-shell, the slack thread composer, the browser chat composer. Hydra detects them server-side; clients send them as ordinary session/prompt requests.
hydra-acp session export writes a session to a *.hydra JSON bundle (meta + history + optional prompt history). hydra-acp session import brings it back into the local daemon as a new cold session. Use this to archive a session before clearing it, share one with a teammate, restore from backup, or move work between machines without running both daemons live.
hydra-acp session export hydra_session_abc --out backup.hydra
hydra-acp session import backup.hydra # → new local id
hydra-acp session import backup.hydra # error: already imported
hydra-acp session import backup.hydra --replace # overwrites in place
Each session carries a stable lineageId that survives every export/import hop, so the same bundle imported twice is detected as a duplicate — the second import errors with the existing local id. --replace overrides that and overwrites the existing local copy, killing any live session first and preserving the local sessionId so bookmarks (Slack threads, editor session links) keep resolving.
The first attach to an imported session is slow: hydra spawns a fresh agent, runs session/new, and feeds the imported history back in as a synthesized takeover transcript (same machinery as /hydra agent). Subsequent attaches use the normal session/load path. This is a text-level handover — the originating agent's internal state (tool-call chains, compacted earlier turns) isn't preserved, so the resumed conversation may be cognitively shallower than the original.
Anything you put after <agent> in launcher mode is forwarded to the underlying agent's command. Hydra appends the extra args to the registry-provided spawn plan. Example:
hydra-acp launch codex-acp -c sandbox_mode=danger-full-access
The daemon spawns npx -y @zed-industries/codex-acp@<version> -c sandbox_mode=danger-full-access. Args survive daemon restart — they're stored alongside the resume hints, so a resurrected session re-spawns its agent with the same arguments.
Every config-knob flag has an HYDRA_ACP_FOO_BAR env-var equivalent. Flag wins over env; env wins over default.
| Flag | Env var |
|---|---|
--name |
HYDRA_ACP_NAME |
--agent |
HYDRA_ACP_AGENT |
--model |
HYDRA_ACP_MODEL |
--session |
HYDRA_ACP_SESSION |
--model is a one-shot override for the per-agent defaultModels entry in ~/.hydra-acp/config.json. It only applies at fresh session creation — resurrect and /hydra agent switch ignore it (resurrected sessions stay on whatever model they were last using).
Action commands (init, daemon, session, extension, transformer, agent, auth, cat, --help, --version, --rotate-token) are not config knobs and are flag-only.
When you ask hydra to spawn an agent (via launch <agent>, --agent, or HYDRA_ACP_AGENT), the daemon first tries an exact match against the ACP Registry's id field. If nothing matches, it falls back to matching against the npx package basename (the segment after the last / and before the version @). That means common binary names work transparently:
| You spawn… | Registry id |
Resolves via |
|---|---|---|
claude-acp |
claude-acp |
exact id |
claude-agent-acp |
claude-acp |
npx package basename claude-agent-acp |
gemini |
gemini |
exact id |
gemini-cli |
gemini |
npx package basename gemini-cli |
codex-acp |
codex-acp |
exact id |
~/.hydra-acp/config.json:
{
"daemon": {
"host": "127.0.0.1",
"port": 55514,
"logLevel": "info"
},
"registry": {
"url": "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json",
"ttlHours": 24
},
"defaultAgent": "claude-code"
}The service token lives in its own file (~/.hydra-acp/auth-token, mode 0600) and is never written to config.json — so the config file stays safe to version-control.
daemon.sessionIdleTimeoutSeconds (default 3600 — one hour) controls how long a session with no recorded agent or user activity stays alive before the daemon closes it. Snapshot-shaped state pings (model/mode/title/commands) and bare attach/detach don't count as activity — only recordable broadcasts (prompts, agent chunks, tool calls, permission prompts) do, so persistent observer clients like the slack/notifier/approver/browser extensions can't pin a quiet session open. In-flight turns and unresolved permission requests defer the close until they settle. The disk record stays so the session can be resurrected later via session/load, at which point extensions re-attach automatically through their poll loops. Set to 0 to disable.
daemon.sessionRecentMinutes (default 30) controls how far back hydra-acp session (and the /v1/sessions REST endpoint without ?all=true) looks for cold (disk-only) sessions. Set to 0 to never list cold sessions.
tui.mouse (default false) controls whether the TUI captures mouse events. With capture off (the default), plain click-drag selects text via your terminal emulator, but wheel-driven scrollback stops working — use PgUp / PgDn instead. Set to true to enable capture, which lets the scroll wheel drive scrollback at the cost of requiring shift+drag to select text.
tui.defaultEnterAction (default "amend") controls what the unmodified Enter key does in the prompt composer. With "amend" (the default), Enter amends the in-flight turn and Shift+Enter enqueues a new prompt; with no turn in flight either key just enqueues, since there's nothing to amend. Set to "enqueue" to flip the two: Enter enqueues (sends immediately when idle, queues behind an in-flight turn) and Shift+Enter amends.
Transformers are a second kind of daemon-managed process. Where an extension is a client — it observes broadcast events and sends prompts — a transformer is middleware: it sits inside the daemon's message pipeline and sees every in-flight ACP message before the daemon acts on it, in both directions.
client → daemon → [T1 → T2 → … → Tn] → agent
client ← daemon ← [Tn ← … ← T1] ← agent
This means a transformer can inspect every prompt before the LLM sees it and every response before it reaches clients or mutates daemon state (model, mode, history). It cannot do this invisibly — the chain is operator-visible and each transformer's intercepts are declared up front.
A transformer is configured in the same way as an extension but under a separate key.
{
"transformers": {
"my-transformer": {
"command": ["node", "/path/to/my-transformer.mjs"]
}
},
"defaultTransformers": ["my-transformer"]
}defaultTransformers lists transformer names applied to every new session, in order — the array is the pipeline. Each message passes through T1, then T2, then T3 before reaching the agent (or clients on the way back). Order matters when transformers interact: a prompt-rewriting transformer should come before a logging transformer so the logger sees the rewritten prompt, not the original. Individual sessions can override the chain via _meta["hydra-acp"].transformers on session/new. The daemon resolves names to their live connections at session-creation time; a transformer that is configured but not yet connected is silently skipped (fail-open).
Each transformer receives:
- the same env vars as extensions (
HYDRA_ACP_TOKEN,HYDRA_ACP_WS_URL, etc.) - a
HYDRA_ACP_TRANSFORMER_NAMEenv var with its config key
A transformer process connects using its own token (same mechanism as extensions) and then registers the message kinds it wants to intercept. For each intercepted message the daemon hands it to the transformer and waits for a continue/stop/processing decision. (The exact JSON-RPC methods and payloads live in PROTOCOL.md.)
See cli/examples/transformer-observe.mjs for a working reference that logs all traffic and always continues, cli/examples/transformer-edit.mjs for one that modifies prompts before they reach the agent, and cli/examples/transformer-lifecycle.mjs for one that reacts to session lifecycle events (session.opened, session.idle, session.closed) and optionally emits a follow-up prompt when a session goes quiet.
Trust model: transformers receive the same per-process scoped token as extensions, but have structurally more access — they intercept traffic that no client ever sees. The transformer-specific methods are only callable with a transformer-kind token; an extension process that tries to call them receives MethodNotFound. Treat every entry in transformers as a higher-trust boundary than extensions.
The service token (stored at ~/.hydra-acp/auth-token, mode 0600) is generated on hydra-acp init and required as Authorization: Bearer <token> for every REST call and as a WebSocket subprotocol or query parameter for wss://.../acp. The token never leaves ~/.hydra-acp/.
For remote access (binding to a non-loopback address), enable TLS via:
{
"daemon": {
"tls": {
"cert": "/path/to/cert.pem",
"key": "/path/to/key.pem"
}
}
}The daemon refuses to bind to non-loopback hosts without TLS configured.
~/.hydra-acp/
├── config.json # daemon config (safe to version-control)
├── auth-token # service token (mode 0600)
├── daemon.pid # PID + port lockfile (when running)
├── daemon.<N>.log # rotated daemon logs (10 MB or daily, whichever first)
├── current.log # symlink to the active daemon.<N>.log
├── registry.json # cached ACP registry (24h TTL)
└── agents/
└── <agent-id>/
├── meta.json # registry entry snapshot
└── ... # agent-specific install (npx cache, binary, etc.)
Logs are also fanned out to stderr while the daemon is running. To follow live: tail -F ~/.hydra-acp/current.log.
The daemon's WSS endpoint follows the WebSocket profile of the Streamable HTTP & WebSocket Transport RFD (the Streamable HTTP profile isn't implemented — see the transport section above):
GET /acp HTTP/1.1
Host: localhost:55514
Upgrade: websocket
Sec-WebSocket-Protocol: acp.v1, hydra-acp-token.<token>
The server selects acp.v1 and echoes it back in Sec-WebSocket-Protocol on the 101 response; the hydra-acp-token.<token> entry is consumed as auth and never echoed. Frames are JSON-RPC 2.0 text frames; binary frames are ignored.
The first JSON-RPC message a client sends is initialize (per ACP).
Standard ACP:
initialize— capability negotiationsession/new— create a new session, spawning the requested agentsession/promptsession/cancelsession/list { cwd?, cursor? }— enumerate sessions known to the daemon
RFD additions:
session/attach { sessionId, historyPolicy: "full"|"pending_only"|"none" }— RFD #533session/detach { sessionId }— RFD #533
Capabilities advertised in the initialize response:
{
"agentCapabilities": {
"promptCapabilities": {
"image": true,
"audio": true,
"embeddedContext": true
},
"mcpCapabilities": {
"http": true,
"sse": true
},
"loadSession": false,
"sessionCapabilities": {
"attach": {},
"list": {}
}
}
}Hydra is a transparent proxy for prompt content and MCP server configs — they're forwarded to the underlying agent unchanged — so the daemon advertises the union of relevant capabilities. The agent ultimately determines what it accepts. If an editor sends a content type the underlying agent rejects, the rejection surfaces as a normal ACP error from the agent, not a hydra-side error.
The daemon exposes three surfaces on a single TCP port (default 127.0.0.1:55514):
- REST API at
/v1/*— management plane (auth, sessions, agents, registry, extensions, transformers, MCP routes). - ACP WebSocket at
/acp— JSON-RPC 2.0. Standard ACP plus the Hydra-specific extensions (prompt queue, stdin streaming, transformer plumbing, …). - Agent-facing MCP at
/mcp/*— Streamable HTTP MCP transport used by spawned agents to reach the per-session stdin ring buffer and extension-contributed tools.
All three are documented in PROTOCOL.md, with endpoint shapes, request/response bodies, JSON-RPC method semantics, capability discovery, and the JSON-RPC error code reservations.
The daemon exposes a process-management surface. Treat the service token like an SSH key.
- Default bind is
127.0.0.1. Cross-host access requires TLS + a strong token. - No anonymous access. Every request — REST and WSS — must present the bearer token.
- Token rotation:
hydra-acp init --rotate-tokeninvalidates the old token; running clients are kicked. - Sandboxing is the user's responsibility. Spawned agents inherit the daemon's filesystem and shell. Run the daemon under a restricted user or inside a container if you don't trust agents fully.
- Subprocess scope: agent processes inherit
cwdand a sanitized environment. The daemon does not pass its service token through to spawned agents.
If accepted, hydra-acp could land in the ACP Registry:
{
"id": "hydra-acp",
"name": "Hydra ACP",
"version": "0.1.0",
"description": "Multi-client session daemon. Spawn agents, attach over WSS, multiplex sessions across editors.",
"authors": ["Sam Magnuson"],
"license": "MIT",
"icon": "icon.svg",
"repository": "https://github.com/smagnuso/hydra-acp",
"website": "https://github.com/smagnuso/hydra-acp",
"distribution": {
"npx": {
"package": "@hydra-acp/cli",
"args": ["shim"]
}
},
"capabilities": {
"session": {
"attach": {},
"list": true,
"proxy": true
},
"transport": {
"stdio": true,
"websocket": true
}
}
}The accompanying icon lives at assets/icon.svg — a single currentColor-filled <path> rendering the three-headed hydra silhouette. The viewBox is 32×32 (so the path supersamples cleanly at the registry's 16×16 display size); the SVG declares width="16" height="16" per the registry rules. Source PNG at assets/hydra-source.png is what the silhouette and the README's top-of-page Braille art are both derived from.
This is an early experiment.
MIT.