ac7 claude-code
ac7 claude-code wraps a Claude Code
session in a ac7 runner. The runner is the parent; claude is the
child. You talk to claude in the same terminal you launched the
runner from.
ac7 claude-code
If --token / $AC7_TOKEN aren’t set, the runner falls back to a
saved ~/.config/ac7/auth.json entry for the resolved broker URL.
Run ac7 connect first if you haven’t.
Synopsis
ac7 claude-code [--no-trace] [--doctor] [--skip-doctor] [--unsafe-tls]
[--url <url>] [--token <secret>] [-- <claude args>...]
| Flag | Type | Default | Description |
|---|---|---|---|
--no-trace | bool | off | Disable trace capture: no MITM proxy, no per-session CA, no env injection. The runner still does briefing + IPC + objectives normally. |
--doctor | bool | off | Run the preflight checks, print the full report, exit. Doesn’t spawn claude. Exit code is 1 if any check FAILed. |
--skip-doctor | bool | off | Skip the silent preflight that runs by default before spawning claude. Useful in CI / scripted reruns where you’ve already validated the environment. |
--unsafe-tls | bool | off | Set NODE_TLS_REJECT_UNAUTHORIZED=0 on the agent child. Disables all TLS validation in the agent process. Escape hatch for packaged-binary Claude builds that can’t honor NODE_EXTRA_CA_CERTS. Sunset-dated. |
--url <url> | string | http://127.0.0.1:8717 | Broker base URL. Falls back to $AC7_URL then the default. |
--token <secret> | string | — | Bearer token. Falls back to $AC7_TOKEN then auth.json. |
-- <claude args>... | passthrough | — | Everything after -- is forwarded verbatim to claude. Anything before -- that the runner doesn’t recognize also flows through. |
What it does on startup
- Locate
claude. Reads$CLAUDE_PATHfirst, falls back towhich claude. Fails fast (no socket bind, no.mcp.jsontouch) if the binary is missing. - Run preflight. Unless
--skip-doctoris set, runs the same four checks--doctordoes, silently. WARN states proceed; any FAIL aborts and dumps the full report to stderr. - Start the runner.
startRunner()fetches the briefing, binds the IPC socket, starts the SSE forwarder. Failures surface as aRunnerStartupErrorwith an actionable hint (e.g. “isac7 serverunning at<url>?” on connection refused). - Start the trace host (unless
--no-trace). Generates a fresh per-session local CA, writes the cert PEM to$TMPDIR/ac7-trace-ca-<pid>-<nonce>.pemat0o600, binds the loopback CONNECT proxy on a random ephemeral port. The CA’s private key never touches disk. - Back up
.mcp.jsonand write a fresh one. The current working directory’s.mcp.jsonis copied to a pid-scoped tmp path, then atomically rewritten with aac7MCP-server entry that spawnsac7 mcp-bridgeagainst this runner’s socket. If no prior.mcp.jsonexisted, a new one is created. - Auto-inject claude flags (see below).
- Spawn
claude. Ifnode-ptyis loadable and stdin/stdout are TTYs, the runner takes the pty path and reserves the bottomHUD_HEIGHTrows for the ac7 status strip. Otherwise it falls back tostdio: 'inherit'and skips the HUD. - Wait for claude to exit. On any exit path (normal,
SIGINT, SIGTERM, uncaughtException, unhandledRejection):
restore the original
.mcp.json, shut down the runner, close the trace host, unlink the socket, delete the CA cert PEM.
The auto-injected claude flags
Three flags are always-on for ac7 sessions because they’re structural requirements rather than convenience. The runner injects them before any user-supplied claude args, and prints a banner to stderr listing what was injected so the posture is visible on turn 1.
| Flag | Why |
|---|---|
--dangerously-skip-permissions | ac7’s MCP tools (broadcast, objectives_*, channels_*, etc.) are designed to be callable without per-call permission prompts. The team’s permission model is the access-control layer, not per-tool yes/no prompts. |
--dangerously-load-development-channels server:ac7 | Enables claude’s claude/channel experimental capability against our bridge (keyed ac7 in the written .mcp.json). Without this, the bridge declares the capability but claude ignores it and broker push events never reach the agent. |
--append-system-prompt <briefing> | Pins the team briefing (team name + directive + brief, role, personal instructions, teammates, open objectives) into claude’s system prompt for every turn. Survives compaction; beats the “agent forgot who it is by turn 40” failure mode. |
If the user passes any of these themselves, the runner suppresses the equivalent injection (and the banner). The briefing snapshot is taken at startup — edits to the team config require restarting the runner to take effect.
ac7: auto-injected into claude invocation (team authority is the access control):
--dangerously-skip-permissions
--dangerously-load-development-channels server:ac7
--append-system-prompt <ac7 briefing, 1843 chars>
(pass either flag yourself to suppress this line)
Environment passed to the agent
When tracing is on, the trace host returns an env delta which the
runner merges into claude’s environment:
HTTPS_PROXY=http://127.0.0.1:<ephemeral-port>
HTTP_PROXY=http://127.0.0.1:<ephemeral-port>
ALL_PROXY=http://127.0.0.1:<ephemeral-port>
NO_PROXY=localhost,127.0.0.1,::1,<caller's value>
NODE_USE_ENV_PROXY=1
NODE_EXTRA_CA_CERTS=$TMPDIR/ac7-trace-ca-<pid>-<nonce>.pem
NODE_OPTIONS=<existing> --loader <ac7 ssl-keylog loader>
SSLKEYLOGFILE=<runner-managed path>
AC7_RUNNER_SOCKET=/tmp/.ac7-runner-<pid>.sock
When --unsafe-tls is set, the runner also injects:
NODE_TLS_REJECT_UNAUTHORIZED=0
and prints a hard-edged warning box to stderr. Use this only
when claude is a packaged binary that ignores NODE_EXTRA_CA_CERTS,
and accept that all TLS validation is off in the agent process for
the entire session. Loopback-MITM traffic going through our
proxy is still validated against the real upstream — just not
against our injected CA.
When --no-trace is set, none of these are injected; the runner
sets only AC7_RUNNER_SOCKET.
The MCP bridge wiring
The runner detects the bridge command from its own process:
bridgeCommand:process.execPath(the running node binary)bridgeArgs:[process.argv[1], 'mcp-bridge'](this CLI’s entry script + the subcommand)
Baking these into .mcp.json means claude spawns the SAME cli that
spawned it — no $PATH assumptions, works identically for global
npm install, pnpm script, or development checkout. The bridge
subprocess reads $AC7_RUNNER_SOCKET to find the runner’s IPC
socket.
The .mcp.json shape written by the runner:
{
"mcpServers": {
"ac7": {
"command": "/path/to/node",
"args": ["/path/to/cli/dist/index.js", "mcp-bridge"],
"env": {
"AC7_RUNNER_SOCKET": "/tmp/.ac7-runner-12345.sock"
}
}
}
}
Any pre-existing mcpServers entries are preserved (the runner
merges, not replaces). On exit, the original file is restored
byte-for-byte.
—doctor checks
ac7 claude-code --doctor runs four checks and prints a
PASS / WARN / FAIL line for each:
| Check | What it validates | Failure means |
|---|---|---|
claude binary | claude is on $PATH or $CLAUDE_PATH resolves | Install Claude Code or set $CLAUDE_PATH |
$TMPDIR writable | We can write the CA cert PEM at 0o600 | Filesystem permission issue, or $TMPDIR set to a path that doesn’t exist |
loopback bindable | listen() succeeds on 127.0.0.1:0 | Sandboxed environment with no loopback (rare; some CI runners) |
trace CA + leaf gen | Exercises the full node-forge cert signing pipeline | Runtime crypto issue (Node version, native module mismatch) |
Exit code is 0 if all checks PASS / WARN, 1 if any FAIL.
The default behavior (without --doctor or --skip-doctor) is to
run these checks silently before spawn and dump the full report
to stderr only if any FAIL. WARNs proceed quietly.
The HUD strip
When node-pty is available and stdin/stdout are TTYs, the runner
relays claude’s output through a pty and reserves the bottom
HUD_HEIGHT (5) rows for a status strip:
┌──────────────────────────────────────────────┐
│ │
│ claude TUI (rows - 5) │
│ │
├──────────────────────────────────────────────┤
│ ac7 ⊙ online · builder@ac7-team │
└──────────────────────────────────────────────┘
The HUD shows presence (connecting → online → offline) plus
the agent’s identity. It’s repainted after every chunk claude
writes (so CSI 2J repaints don’t leave it stale) and on terminal
resize. SIGWINCH is forwarded as pty.resize(cols, rows - HUD_HEIGHT) so claude’s renderer never paints into the strip.
When node-pty isn’t loadable (the package is in
optionalDependencies so it may be absent on some platforms), the
runner falls back to stdio: 'inherit' and skips the HUD. Sessions
remain functional; only the strip disappears.
When stdin/stdout aren’t TTYs (tests, piped input), the runner
also falls back to stdio: 'inherit' so automation stays
byte-for-byte compatible.
First-render quirk on the pty path: claude-code’s ink fork blocks its first render on a terminal-capability probe whose reply doesn’t materialize under a relay. Press Enter once after the banner to unblock it; the keystroke is forwarded to claude as a no-op submit on the welcome prompt.
Pass-through args to claude
Anything the runner doesn’t recognize flows through to claude:
# These are equivalent:
ac7 claude-code --model claude-3-opus-20250219
ac7 claude-code -- --model claude-3-opus-20250219
The literal -- is optional but recommended for unambiguity when
forwarded args might collide with future ac7 flags.
Session log
Every run writes structured JSON to a session log under
~/.cache/agentc7/:
~/.cache/agentc7/session-claude-code-<pid>.log
The path is printed on startup so you can tail -f it for live
diagnostics. Each line is one event: runner state changes, IPC
frames, MCP requests, trace uploads, broker errors. Stdout stays
clean for claude; stderr is human-facing notices only.
Common patterns
# First-time on a new device
ac7 connect
ac7 claude-code
# Pinned model + extra claude args
ac7 claude-code --model claude-3-opus-20250219 -- --resume
# Quick sanity check before spawning
ac7 claude-code --doctor
# Skip preflight in a known-good environment
ac7 claude-code --skip-doctor
# Disable trace (debugging the runner/bridge plumbing)
ac7 claude-code --no-trace
# Packaged-binary Claude that can't honor NODE_EXTRA_CA_CERTS
ac7 claude-code --unsafe-tls