This page describes current behaviour only. ccxray has no official keep-alive or service option today; whether to ship one is undecided.
Only one process can listen on a port, and it is one of two kinds:
| Hub | Standalone | |
|---|---|---|
| Started by | ccxray <agent> on macOS/Linux without --port (forks a detached --hub-mode process) |
ccxray with no agent; ccxray --port N <agent>; every agent launch on Windows; the Herdr plugin when no proxy is running (ccxray --no-browser, detached) |
hub.json / hub.sock |
Written after it listens; removed on shutdown | Never written |
| Client registry | Yes — ccxray <agent> launchers register and unregister |
None |
| Shutdown | Idle shutdown IDLE_TIMEOUT_MS = 5 s (server/hub.js, not configurable) after it becomes ready with no clients, or after the last client leaves. A client whose pid dies without unregistering is dropped by a sweep every 30 s. |
No idle shutdown. ccxray with no agent runs until stopped. ccxray --port N <agent> exits when its agent exits. |
ccxray status |
Hub: http://localhost:<port> … |
No hub running. plus Note: port <port> is held by a standalone (non-hub) ccxray and Machine: {"proxy":true,"hub":false,…} |
GET /_api/health |
hub: true |
hub: false |
GET /_api/health also returns version and gitSha (the 40-hex commit the process started from when the package root is a git checkout, else null; captured at startup before the port accepts requests, public, no other git data; reftable repositories and symlinked or malformed git metadata report null) — use gitSha to confirm a restart picked up a merge.
Both kinds listen on loopback only by default: 127.0.0.1, plus ::1 on the same port when the host has IPv6 loopback. A client on the same machine — or one arriving over an SSH reverse tunnel, which reaches ccxray from 127.0.0.1 — needs no change. To accept other hosts, set PROXY_HOST (for example PROXY_HOST=0.0.0.0); those clients must send a ccxray credential (see SECURITY.md). In hub mode the hub inherits PROXY_HOST from the shell that forked it.
- A hub lives only while it has registered clients, plus 5 s. Pointing an agent at a hub's port with
ANTHROPIC_BASE_URL(or the Codex/Grok equivalents below) does not register it, so it does not keep the hub alive: when the lastccxray <agent>client exits, the hub exits and the port closes. - A standalone
ccxray(no agent) stays up until you stop it. That is the only way, today, to keep a proxy running independently of any launched agent. Running it under a supervisor of your choosing is outside what ccxray provides or tests. - Hub clients recover from a hub crash: each
ccxray <agent>client checks the hub pid every 5 s and forks a new hub when it is gone.
ccxray is in the request path; there is no bypass. If nothing is listening on the port an agent was pointed at, its requests fail with a connection error. They are not forwarded upstream and not recorded.
At launch time, ccxray <agent> is different: if it cannot attach to or start a proxy (attach budget CCXRAY_ATTACH_TIMEOUT_MS, default 3000 ms), it starts the plain agent without the proxy and records the fallback in <CCXRAY_HOME>/fail-open.log. That session is not proxied. CCXRAY_FAIL_CLOSED=1 exits instead.
-
ccxray <agent>finds a standalone on the port. There is nohub.json, so the launcher forks a hub. The hub cannot bind the port, retries for 5 s, and logs the occupant tohub.log(port … is held by a standalone (non-hub) ccxray). The launcher then fails open: the plain agent runs unproxied, recorded infail-open.log(or exits withCCXRAY_FAIL_CLOSED=1). It does not reuse the standalone and does not stop it. Use--port(independent server) orPROXY_PORT(hub on another port) to run alongside it. -
A standalone finds a hub on the port.
ccxrayexits withError: port <port> is already in use; the hub is untouched. -
Herdr plugin launch actions (
Launch Claude/Codex/Grok via ccxray) choose a proxy in this order, honouringPROXY_PORTwhen set (a proxy on another port is not used):- a hub reported by
ccxray status— the launch registers the new pane's shell (fromherdr pane process-info) as a hub client, so the hub stays up while that pane is open; the hub's dead-client sweep removes it after the pane closes; - a standalone ccxray already on the port — reused as is;
- otherwise it starts a detached standalone
ccxray --no-browser, which stays up after the agent exits.
Because of step 3, a later
ccxray <agent>on the same port meets the standalone case above. The agent itself is started natively by Herdr, not throughccxray <agent>. - a hub reported by
Agents that are not started through ccxray <agent> — for example a runner that spawns claude -p, codex exec, or grok — must be pointed at the proxy themselves. Loopback requests need no token unless CCXRAY_LOOPBACK_REQUIRE_AUTH=1 (see SECURITY.md). These settings mirror what server/providers.js passes when ccxray launches each agent. Remember they do not register with a hub (see above).
| Agent | Setting |
|---|---|
| Claude Code | ANTHROPIC_BASE_URL=http://localhost:5577 |
| Codex (API key) | A custom model provider whose base_url is the proxy's /v1: codex -c 'model_providers.ccxray={name="ccxray", base_url="http://localhost:5577/v1", wire_api="responses"}' -c 'model_provider="ccxray"' … (or the same model_providers.<id> table in config.toml). |
| Codex (ChatGPT login) | codex -c 'openai_base_url="http://localhost:5577/v1"' … only. Do not set chatgpt_base_url to the proxy: ChatGPT workspace discovery rejects a loopback URL (#671) |
| Grok CLI | GROK_CLI_CHAT_PROXY_BASE_URL=http://localhost:5577/v1 |
Use your proxy's port in place of 5577.
Runs that bypass the proxy are not recorded live. Claude Code and Codex transcripts can be imported afterwards (ccxray import --once, or automatic startup import with CCXRAY_IMPORT_DISABLE=0). The importer reads only Claude and Codex transcripts; there is no Grok importer, so an unproxied Grok run is not recoverable.
- Startup pruning (
LOG_RETENTION_DAYS, default 14) deletes old_req.json/_res.jsonfiles and then drops index lines whose proxy files are gone. Imported lines have no such files, so they survive pruning. The exception: an imported line that duplicates a proxy line (sameresponseId) is dropped once that proxy line's files are pruned. - The dashboard's startup restore loads only index lines from the last
RESTORE_DAYS(default:LOG_RETENTION_DAYS, i.e. 14) into memory. Older sessions, imported or not, still appear as session cards from the session index (sessions.json), and their turns are read fromindex.ndjsonwhen the session is opened.ccxray usageandccxray agentflow reportreadindex.ndjsondirectly and are not limited byRESTORE_DAYS.
All variables mentioned here are listed in env-vars.md.
Spawned, cron and scripted claude/codex launches bypass the shell wrapper. See launcher shims (ccxray setup --wrap-binaries) for the fail-open way to record them, what it covers, and its trade-off against fail-closed tool config.