Skip to content

smagnuso/hydra-acp

Repository files navigation

hydra-acp

⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⢃⣤⡶⠂
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣿⣶⣤⡀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠄⠀⠀⠀⠀⠀⢀⣠⣶⣿⣥⣾⣿⣿⣿⣿⣿⣍⠀⠀⠀⠀⠀⠀⠀⠀⠀⣄⡀
⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣯⣴⡾⠋⠀⠀⠀⠸⡿⠟⠋⣹⣿⡿⢿⣿⣿⣿⣿⢧⠀⠀⠀⠀⠀⠙⢷⣦⣼⣿⣄
⠀⠀⠀⠀⠀⠀⣰⡾⠟⣿⣿⣿⣿⣿⣶⠄⠀⠀⢀⣠⢾⡿⠉⠀⢨⣿⣿⣿⣿⡄⠀⠀⠀⠰⣶⣿⣿⣿⣿⣿⠿⣷⣄
⠀⠀⠀⠀⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠈⠃⠀⠀⠀⠀⣼⣿⣿⣿⡏⠁⠀⠀⣴⣾⣿⣿⣿⣿⣿⣿⣷⣾⣿⣷⣦
⠀⠀⠀⠀⠙⠋⠀⣠⣿⠿⠛⠉⢻⣿⣿⣿⣧⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⡿⠁⠀⠀⢠⣾⣿⣿⣿⠟⠉⠻⢿⣷⡀⠘⠹⠋
⠀⠀⠀⠀⠠⢴⠋⠻⠇⠀⠀⠀⠀⣿⣿⣿⣿⠃⠀⠀⠀⢀⣾⣿⣿⣿⣿⠃⠀⠀⠀⢻⣿⣿⣿⡏⠀⠀⠀⠀⠿⠟⠳⡤
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⡆⠀⠀⠀⣼⣿⣿⣿⣿⡇⠀⠀⠀⢀⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠁
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣄⠀⢰⣿⣿⣿⣿⣿⡇⠀⠀⣠⣿⣿⣿⣿⡟
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣷⣼⣿⣿⣿⣿⣿⣷⣴⣾⣿⣿⣿⣿⡟
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡋⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠃
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠐⢻⣿⠿
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠘⣿⡄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣁⣀⠀⠀⣿⣿⡄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⡏⠙⠻⠿⣿⣿⣿⣿⢿⣿⣿⣿⡿⣿⣿⣿⣿⣿⣿⣧⣤⣾⣿⣿⠃
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⡿⠁⠀⠀⢀⣿⣿⣿⠃⠸⣿⣿⣿⠃⠈⢻⣿⣿⡿⢿⣿⣿⡿⠟⠃
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣾⣿⣿⡿⠀⠀⢠⢶⣾⣿⡿⠃⣠⣴⣿⣿⣿⠀⢠⣶⣿⣿⣷
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠉⠛⠙⠋⠉⠁⠀⠀⠀⠀⠀⠁⠀⠀⠋⠙⠉⠟⠉⠀⠈⠈⠁⠉

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.

What it is

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 session

That'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.

What it gives you

  • 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.

How it's built

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.

The standards it stitches together

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. historyPolicy controls 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.

Standard ACP it relies on

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.

The registry it depends on

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.

Architecture

            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

How it works

  1. Editor spawns hydra-acp as it would any ACP agent. The shim looks like a normal stdio agent.
  2. Shim opens a WSS connection to the daemon at /acp, authenticating via the bearer token.
  3. session/new from 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.
  4. session/attach from a second client → daemon adds the new client to the session's broadcast list and replays history per historyPolicy (per RFD #533).
  5. 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/update with sessionUpdate: "permission_resolved" carrying the resolving client's outcome.
  6. session/list returns the daemon's sessions (live and cold), filterable by cwd.
  7. session/detach lets a client leave voluntarily; the session continues until the last client detaches (per RFD #533).

Why a shim?

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.

Cat mode

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 the hydra-acp-stdin MCP 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-stdin MCP server: bytes flow into a ring buffer and the agent pulls them on demand via head, tail, grep, read, and info. A multi-gigabyte log isn't a context-window problem; it's a fixed-size buffer the agent samples.
  • --follow for live streams. Pipe tail -f into --follow and 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.
  • --detach to share the session. By default the session lives as long as the cat process; on stdin EOF it dies. With --detach it stays in the daemon, hydra-acp session lists 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.

Install

npm install -g @hydra-acp/cli

Drops 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

Quick start

# 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_abc123

Extensions

Hydra 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-slack

You'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-browser

The 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-approver

Without 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-archiver

See 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-planner

See the package README for the full set of /hydra planner verbs (status, retry, cancel, …) and the task-DAG format.

Configuring extensions

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.

CLI

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.

Launcher mode

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.

Naming sessions from the editor

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.

Read-only viewer (--readonly)

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 (typed in any composer)

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.

Exporting and importing sessions

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.

Forwarding agent args (hydra-acp launch <agent> ...)

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.

Flag/env equivalence

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.

Registry id resolution

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

Config

~/.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

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_NAME env 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.

Disk layout

~/.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.

Wire protocol

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).

Methods implemented

Standard ACP:

  • initialize — capability negotiation
  • session/new — create a new session, spawning the requested agent
  • session/prompt
  • session/cancel
  • session/list { cwd?, cursor? } — enumerate sessions known to the daemon

RFD additions:

  • session/attach { sessionId, historyPolicy: "full"|"pending_only"|"none" } — RFD #533
  • session/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.

Protocol surfaces

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.

Security

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-token invalidates 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 cwd and a sanitized environment. The daemon does not pass its service token through to spawned agents.

Registry entry mockup

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.

Status

This is an early experiment.

License

MIT.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors