npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hydra-acp/cli

v0.1.45

Published

Multi-client ACP session daemon: spawn agents, attach over WSS, multiplex sessions across editors.

Readme

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 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 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 (the heads)
              │             │             │
            stdio           │             │
              │             │             │
       hydra-acp shim       │             │
              │             │             │
              └─────────────┼─────────────┘
                            │
                       WSS / HTTP
                            │
                    hydra-acp daemon                ← daemon (the body)
                            │
              ┌─────────────┼─────────────┐
              │             │             │
       ACP agent A      ACP agent B   ACP agent C   ← agents (the feet)
       (one per session, sourced from the ACP Registry)

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.

Surviving daemon restarts (resurrection)

The shim and daemon together implement a "resume hint" pattern that lets editor sessions survive a daemon restart without the editor noticing:

  1. The daemon's session/new and session/attach responses include a _meta block, with hydra-specific data namespaced under _meta["hydra-acp"] (per the ACP Extensibility convention). The underlying agent's own _meta keys, if any, are passed through unchanged alongside hydra-acp.
  2. The shim caches that namespaced data in a SessionTracker as messages flow through, keyed by the hydra sessionId the editor knows.
  3. The shim's WS connection is wrapped in a ResilientWsStream that reconnects with exponential backoff (200ms → 5s, capped, max 60 attempts) and buffers outbound messages from the editor while disconnected.
  4. After each successful reconnect, the shim replays a session/attach for every cached session, including the resume hints under _meta["hydra-acp"].resume.
  5. If the daemon already knows the session (e.g., the daemon never died, just a network blip), it ignores the resume hint and does a normal attach.
  6. If the daemon doesn't know the session, it resurrects: spawns a fresh agent of agentId in cwd, runs initialize, calls ACP session/load { sessionId: upstreamSessionId } against the agent, and registers a new hydra Session with the same hydra sessionId the shim claimed. The editor sees nothing.

The resurrection is serialized per hydra sessionId, so two shims racing to reattach to the same session don't both spawn fresh agents.

What this requires: the underlying agent must support loadSession and persist its own session state to disk between processes (e.g., claude-code-acp does, in ~/.claude/sessions/). For agents that don't support load, resurrection fails on the daemon side and the shim surfaces an error to the editor.

What gets lost across restart: the daemon's in-memory streaming history and in-flight tool calls. The agent's persisted state — past completed turns, conversation context — is recovered via session/load. The agent will need to re-issue any tool call that was mid-stream when the daemon died.

In-flight permission prompts: the shim tracks open session/request_permission requests it has forwarded to the editor. On any reconnect (which always implies the previous daemon-side promise is gone), the shim emits a session/update notification with sessionUpdate: "permission_resolved" toward the editor for each pending request, carrying the original toolCallId plus outcome: { kind: "cancelled", reason: "daemon-disconnected" } and resolvedBy: { clientId: "hydra-acp" }. Editors that handle permission_resolved per RFD #533 will dismiss their in-flight permission UI. Any response the editor still sends afterward is silently dropped by the new daemon (unknown request id).

Wire shape of _meta

A hydra session/new response looks like:

{
  "sessionId": "hydra_session_abc123",
  "_meta": {
    "agent-vendor": { "sequence": 7 },
    "hydra-acp": {
      "upstreamSessionId": "u_xyz",
      "agentId": "claude-code",
      "cwd": "/path/to/project"
    }
  }
}

The agent-vendor key (illustrative) is whatever the underlying agent put in its _meta block — hydra forwards that through unchanged. Only the hydra-acp namespace is hydra's. The same shape applies to session/attach responses.

For resurrection, the shim sends session/attach with a resume hint nested in the same namespace:

{
  "sessionId": "hydra_session_abc123",
  "historyPolicy": "pending_only",
  "_meta": {
    "hydra-acp": {
      "resume": {
        "upstreamSessionId": "u_xyz",
        "agentId": "claude-code",
        "cwd": "/path/to/project"
      }
    }
  }
}

Install

npm install -g @hydra-acp/cli

Drops hydra-acp (and hydra) on your PATH.

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

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 logs [-f] [-n N]           # tail (default 50) or follow the daemon log

hydra-acp session [list]                   # list sessions
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 logs <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 logs <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 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 rewrites the params to { ..., agentId: "claude-code" } before forwarding to the daemon. 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"].name 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 readonly: true attach to a cold session takes a viewer path that streams history straight from disk — no manager.resurrect, no agent process. Any mutating JSON-RPC method (session/prompt, session/cancel, session/set_model, and the hydra-acp/* prompt-mutation methods) sent from a read-only connection is refused with -32011 PermissionDenied. History replay and session/update deliveries are unchanged, so the existing scrollback search (^R when scrolled back) works over the full transcript.

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.

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.

Configure in ~/.hydra-acp/config.json:

{
  "extensions": {
    "hydra-acp-slack": {},
    "hydra-acp-web-ui": {
      "command": ["hydra-acp-web-ui"],
      "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 logs 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.

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.

Optional extensions

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

Per-extension config (env vars, args, custom command paths) goes in the same extensions block in ~/.hydra-acp/config.json — see the snippet above. hydra-acp extension logs <name> -f tails an extension's stdout/stderr if you need to debug.

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. ```json { "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 calls `transformer/initialize` declaring the message kinds it wants to intercept. For each intercepted message the daemon calls `transformer/message` and waits for `{ action: "continue" }`. Future phases will add `stop` (block the message) and `processing` (transformer handles the request itself).

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. `transformer/initialize` and all 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:

```json
{
  "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.

REST API

All REST endpoints require Authorization: Bearer <token>.

GET    /v1/health                 # liveness
GET    /v1/sessions               # list sessions
POST   /v1/sessions               # create session (alternative to ACP session/new)
POST   /v1/sessions/:id/kill      # demote a live session to cold (keeps the on-disk record); idempotent
DELETE /v1/sessions/:id           # remove a session entirely (live or cold)
GET    /v1/sessions/:id/export    # download a session bundle (*.hydra JSON; meta + history)
POST   /v1/sessions/import        # body { bundle, replace? } → { sessionId, importedFromSessionId, replaced }
                                  # 409 with existingSessionId on a lineageId clash unless replace:true
GET    /v1/agents                 # list known agents (registry + installed)
POST   /v1/agents/:id/install     # pre-install an agent
GET    /v1/registry               # current cached registry contents
POST   /v1/registry/refresh       # force refresh

GET    /v1/extensions             # list configured extensions and live state
POST   /v1/extensions             # register a new extension (takes effect immediately)
DELETE /v1/extensions/:name       # unregister and stop an extension
POST   /v1/extensions/:name/start
POST   /v1/extensions/:name/stop
POST   /v1/extensions/:name/restart

GET    /v1/transformers           # list configured transformers and live state
POST   /v1/transformers           # register a new transformer (takes effect immediately)
DELETE /v1/transformers/:name     # unregister and stop a transformer
POST   /v1/transformers/:name/start
POST   /v1/transformers/:name/stop
POST   /v1/transformers/:name/restart

Sessions are also reachable via session/list over ACP itself, for clients that prefer the protocol-native path.

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 would land in the ACP Registry under either agent.json or extension.json (TBD with maintainers — likely extension.json, since hydra is a session-multiplexer rather than an LLM-backed coding agent).

{
  "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.