Workspace keys (rk_live_), agent tokens (at_live_), node tokens (nt_live_), and the cloud access token leak through four surfaces. This blocks fleet-wide credential rotation — rotated keys would re-leak immediately. Closes when the platform fix (credentials via env/file + redaction at the serialization boundary + explicit file modes) lands.
1. Secrets on child argv (visible in ps to any local user)
- The broker builds the agent-relay MCP config with
at_live_ embedded (crates/broker/src/snippets.rs:326-334) and splices rk_live_ in for Claude (inject_api_key_into_mcp_json, snippets.rs:407), then pushes the whole JSON as one argv element: --mcp-config <json> (snippets.rs:837-850).
- Codex gets one
--config key="value" argv pair per secret (snippets.rs:867-955).
- Exposure is doubled: the broker re-execs itself (
crates/broker/src/worker.rs:310), so the JSON appears on the pty-worker argv and again on the real harness argv.
agent-relay-broker also takes --api-key/--agent-token as CLI flags (crates/broker/src/cli/mod.rs:187) and cli_mcp_args.rs:30 prints the computed config including secrets to stdout.
- File-based writers already exist as the fix seam:
ensure_agent_relay_mcp_config() (snippets.rs:69, currently referenced only by its own tests), ensure_opencode_config_with_result(), ensure_cursor_mcp_config(). Note .claude/rules/mcp-injection.md currently mandates inline embedding and must change with the fix; .cursor/mcp.json is not gitignored despite carrying live keys.
2. node up / node status print keys in plaintext
packages/cli/src/cli/lib/broker-lifecycle.ts:1521 — Workspace Key: ${relay.workspaceKey} unconditionally on every node up (pinned by seven test assertions in core.test.ts).
node status repeats it (broker-lifecycle.ts:1795-1796) and prints an observer URL with the key as a query param.
--workspace-key is passed on the broker argv redundantly (packages/harness-driver/src/spawn-config.ts:125 — the same key is already in the env block), and that argv is folded into every startup error string (broker-process.ts:148).
--verbose prints the handshake message containing the key and the full spawn command line (packages/harness-driver/src/client.ts:466, :373).
- Neither log sink redacts anything:
packages/utils/src/logger.ts (0644 append, bare JSON.stringify) and the Rust tracing appenders (crates/broker/src/runtime/util.rs:144-182, 0644).
3. Unredacted JSON output
agent-relay cloud session --json emits the raw cloud access token (packages/cli/src/cli/commands/cloud.ts:540). Sibling commands redact by explicit design (cloud enroll --json at cloud.ts:729-735 — "Never print the node token, even in JSON mode"; cloud worker status --json at cloud-worker.ts:361), so this is an oversight, not policy.
workspace active --json prints key + relaycastApiKey (workspace.ts:44-47); workspace create prints the key (workspace.ts:68); cloud-room.ts:561 prints a bare invite token; cloud.ts:720 dumps a record including nodeToken to stderr on a failure path.
- Redaction helpers exist but are effectively dead:
crates/broker/src/redact.rs:13 has zero call sites; the only helper that knows the live-key prefixes is private to node_control.rs (:418, one call site); packages/cli/src/cli/lib/redact.ts:24 fires once and is value-blind (an rk_live_ under a key named url/env/args passes through). No masking utility (last4-style) exists anywhere.
4. World-readable credential files at rest
nt_live_ node tokens: crates/broker/src/node_control.rs:1242 — fs::write with no mode → 0644 in a 0755 dir (~/Library/Application Support/agent-relay/node-tokens/); 91 such files observed on one dev machine.
opencode.json and .cursor/mcp.json (both carrying rk_live_ + at_live_): 0644 via write_pretty_json (snippets.rs:1486).
connection.json is 0600 only by accident of tempfile's default (crates/broker/src/runtime/init.rs:153-170) — no test asserts the mode.
~/.agentworkforce/relayhistory/auth.json (access + refresh tokens) has a 0644 window before chmod (packages/cli/src/cli/commands/reflex.ts:117-119).
- The
.agentworkforce/relay parent dir is created without a mode at four of five call sites, so the mode: 0o700 at project-workspace-key.ts:67 is a no-op in practice — observed drwxr-xr-x on disk.
- The TypeScript cloud store does this right (0700 dir + 0600 file + atomic rename,
packages/cloud/src/auth.ts:139-160); the Rust side has zero explicit mode arguments anywhere.
🤖 Generated with Claude Code
Workspace keys (
rk_live_), agent tokens (at_live_), node tokens (nt_live_), and the cloud access token leak through four surfaces. This blocks fleet-wide credential rotation — rotated keys would re-leak immediately. Closes when the platform fix (credentials via env/file + redaction at the serialization boundary + explicit file modes) lands.1. Secrets on child argv (visible in
psto any local user)at_live_embedded (crates/broker/src/snippets.rs:326-334) and splicesrk_live_in for Claude (inject_api_key_into_mcp_json,snippets.rs:407), then pushes the whole JSON as one argv element:--mcp-config <json>(snippets.rs:837-850).--config key="value"argv pair per secret (snippets.rs:867-955).crates/broker/src/worker.rs:310), so the JSON appears on the pty-worker argv and again on the real harness argv.agent-relay-brokeralso takes--api-key/--agent-tokenas CLI flags (crates/broker/src/cli/mod.rs:187) andcli_mcp_args.rs:30prints the computed config including secrets to stdout.ensure_agent_relay_mcp_config()(snippets.rs:69, currently referenced only by its own tests),ensure_opencode_config_with_result(),ensure_cursor_mcp_config(). Note.claude/rules/mcp-injection.mdcurrently mandates inline embedding and must change with the fix;.cursor/mcp.jsonis not gitignored despite carrying live keys.2.
node up/node statusprint keys in plaintextpackages/cli/src/cli/lib/broker-lifecycle.ts:1521—Workspace Key: ${relay.workspaceKey}unconditionally on everynode up(pinned by seven test assertions incore.test.ts).node statusrepeats it (broker-lifecycle.ts:1795-1796) and prints an observer URL with the key as a query param.--workspace-keyis passed on the broker argv redundantly (packages/harness-driver/src/spawn-config.ts:125— the same key is already in the env block), and that argv is folded into every startup error string (broker-process.ts:148).--verboseprints the handshake message containing the key and the full spawn command line (packages/harness-driver/src/client.ts:466,:373).packages/utils/src/logger.ts(0644 append, bareJSON.stringify) and the Rust tracing appenders (crates/broker/src/runtime/util.rs:144-182, 0644).3. Unredacted JSON output
agent-relay cloud session --jsonemits the raw cloud access token (packages/cli/src/cli/commands/cloud.ts:540). Sibling commands redact by explicit design (cloud enroll --jsonatcloud.ts:729-735— "Never print the node token, even in JSON mode";cloud worker status --jsonatcloud-worker.ts:361), so this is an oversight, not policy.workspace active --jsonprintskey+relaycastApiKey(workspace.ts:44-47);workspace createprints the key (workspace.ts:68);cloud-room.ts:561prints a bare invite token;cloud.ts:720dumps a record includingnodeTokento stderr on a failure path.crates/broker/src/redact.rs:13has zero call sites; the only helper that knows the live-key prefixes is private tonode_control.rs(:418, one call site);packages/cli/src/cli/lib/redact.ts:24fires once and is value-blind (anrk_live_under a key namedurl/env/argspasses through). No masking utility (last4-style) exists anywhere.4. World-readable credential files at rest
nt_live_node tokens:crates/broker/src/node_control.rs:1242—fs::writewith no mode → 0644 in a 0755 dir (~/Library/Application Support/agent-relay/node-tokens/); 91 such files observed on one dev machine.opencode.jsonand.cursor/mcp.json(both carryingrk_live_+at_live_): 0644 viawrite_pretty_json(snippets.rs:1486).connection.jsonis 0600 only by accident oftempfile's default (crates/broker/src/runtime/init.rs:153-170) — no test asserts the mode.~/.agentworkforce/relayhistory/auth.json(access + refresh tokens) has a 0644 window before chmod (packages/cli/src/cli/commands/reflex.ts:117-119)..agentworkforce/relayparent dir is created without a mode at four of five call sites, so themode: 0o700atproject-workspace-key.ts:67is a no-op in practice — observeddrwxr-xr-xon disk.packages/cloud/src/auth.ts:139-160); the Rust side has zero explicit mode arguments anywhere.🤖 Generated with Claude Code