Trust boundaries and threat model
ContextRelay is local developer tooling, not an OS sandbox and not a hosted service. It coordinates Claude Code and Codex inside one workstation and one project at a time. Knowing exactly what that means - what ContextRelay protects, what it deliberately leaves to you, and how problems surface - is the difference between trusting it appropriately and over-trusting it.
This page is a faithful summary of the project's
docs/THREAT_MODEL.md,
which is the authoritative document. If anything here ever drifts from that file,
the threat-model document wins.
The trust model in one sentence
Claude Code and Codex are trusted local processes that run with your account's privileges; ContextRelay is the mediation and record-keeping layer between them and the host. Everything inside your operator account is trusted to act with your authority. Everything outside the host should be unable to reach the daemon.
Operator account on one workstation
Claude Code + plugin <---- loopback WS ----\
\
Codex TUI / app-server <---- loopback -----> ContextRelay daemon
/
Codex MCP stdio server ---- loopback WS ----/
Browser viewer ------- loopback HTTP -------/
Local project state: .contextrelay/state
Local registry: OS app/state directory
Every control endpoint binds to 127.0.0.1 and is token-authenticated. The
daemon, the Codex proxy, and the browser viewer are not reachable from another
host. Tokens are split by purpose (control, proxy, viewer), stored in
owner-only files, and compared with constant-time checks.
What is in scope
- A single trusted operator on a single workstation.
- Claude Code connected through the ContextRelay Claude plugin.
- Codex connected through its app-server path and, optionally, the ContextRelay
Codex MCP stdio server (
ctxrelay codex-mcp install). - A daemon bound to loopback with project-scoped state under
.contextrelay/state. - Local tokens and identity checks that prevent accidental cross-project daemon reuse.
- Optional read-only backup agents - only when autonomy is explicitly enabled.
What is out of scope
- Remote access. The daemon and Codex proxy must not be reachable off-host.
- Multi-user / multi-tenant use. There is no RBAC layer.
- Protection from malware or another process running as the same OS user. Such a process can read local tokens, ledger files, source, and terminal state.
- Sandboxing Claude, Codex, backup agents, shell commands, or code under test.
- A tamper-proof audit trail. The ledger is local operational state.
Controls that are in place
These are real, enforced behaviors - not aspirations.
| Control | What it does |
|---|---|
| Loopback bind | Daemon, Codex proxy, and viewer listen only on 127.0.0.1, preventing remote ingress. |
| Split tokens + lockout | Separate control / proxy / viewer tokens, constant-time comparison, and an auth lockout that slows repeated bad attempts. |
| Daemon + project identity | A daemon is bound to its expected state directory and refuses reuse across projects or state dirs. |
| Owner-only file modes | State dir 0700, secret files 0600, where the OS supports it. |
| Frame validation + rate limits | Malformed control frames are rejected; control messages are size- and rate-capped. |
| Coordinator git-write policy | A single agent (collaboration.coordinator) owns git writes; non-coordinators are expected to hand off. |
| Declared permission policy | Eight capabilities (read, write, shell, network, git, secrets, browser, external_api) plus a readonly flag. This states the intended boundary and is surfaced to both agents; it is not enforced at runtime. See below. |
| Read-only-by-default autonomy | Backup agents are OFF until you run ctxrelay autonomy on; even then they are read-only. |
Fail-closed act:write | Autonomous edits are OFF by default behind a layered, fail-closed gate chain (detailed below). |
| Log redaction | Common secret patterns are redacted on rejection-path logs, and in ledger entries - message bodies plus structured handoff, artifact, and runtime-event fields. Pattern-based, so a safety net rather than a guarantee. |
Three of these are worth understanding in depth before you rely on them.
Control-message limits
The control WebSocket caps both message size and message rate per connection, and bounds relay recursion depth, so a runaway or hostile client cannot flood or deeply re-enter the daemon:
CONTEXTRELAY_MAX_CONTROL_MESSAGE_BYTES- default1000000(max accepted message size).CONTEXTRELAY_MAX_CONTROL_MESSAGES_PER_MINUTE- default120(per-connection rate limit).CONTEXTRELAY_MAX_DEPTH- default3(maximum relay recursion depth).
The permission policy is declared, not enforced
This is the most important limitation on this page, so it is stated plainly: setting a capability to denied does not stop an agent from performing it.
ContextRelay does not sit between an agent and its own tools. When Claude Code or Codex runs a shell command, writes with git, or makes a network request, it does so through its own runtime. The daemon never observes that call and has no opportunity to intercept it. There is no syscall interception, no process supervision, and no sandbox around the provider CLIs.
What the policy does provide is an inspectable, auditable record of the boundary
the operator intended, via contextrelay permissions status and the config
file. Be aware it is not currently surfaced to the agents either: the generated
instruction blocks refer to runtime permissions only in general terms and do not
carry your capability list, so an agent cannot read your specific settings even
if it wanted to comply.
Where enforcement is real, it is because ContextRelay owns the surface outright:
act:writeconsults the declaredwritecapability before an autonomous worker is created — for the lane owner and for the Codex executor, sinceagentOverridescan restrict them differently. It then runs in an ephemeral git worktree behind the fail-closed gate chain below, and is OFF by default.contained_runis read-only by construction and only accepts registered contained adapters.
Note that git is deliberately not consulted for act:write: the contained
worker is instructed not to run git at all and leaves its changes uncommitted.
The temporary worktree and branch are containment plumbing performed by the
daemon, not the agent exercising a git capability.
Coordinator git policy is likewise a convention, not a lock — see the row above, which says non-coordinators are expected to hand off.
If you need a boundary that holds against an agent which ignores its instructions, it must come from your provider's own approval and sandboxing controls, or from running the pair inside a container or VM. Treat this policy as intent, and provider controls as enforcement.
act:write is fail-closed by design
act:write lets an autonomous idle worker edit files. It is default-off and
built to fail closed. A write is authorized only when the global master switch and the public surface arm it and every internal floor passes, in order:
- The global
autonomy.enabledmaster switch is on -ctxrelay autonomy offdisables act:write outright, regardless of the two knobs below. autonomy.writableAction.enabledistrue.autonomy.writableAction.budgetUsdis a positive daily cap (the per-task estimate is derived from it internally).- The opportunity is a
dirty_tree_finalizablekind - the only writable kind. - The opportunity owner is
claudeorcodex. - Strict dual-idle quiescence (both agents idle), single-flight, and worktree containment.
- The contained worker runs inside Codex's
workspace-writeOS sandbox (which confines writes to the worktree and the system temp area). - act:write refuses to run if the primary repo is itself under a system-temp
directory (
os.tmpdir(),$TMPDIR,/tmp,/private/tmp).
The per-day USD cap is budgetUsd, AND-ed in by the daemon via the spend ledger. A missing or
invalid cap, an invalid cost estimate, an unreadable or malformed spend ledger, or
an over-cap projection all fail closed.
Containment is the worktree boundary, restricted tooling, and Codex's
workspace-write OS file-write sandbox - not a general-purpose OS jail around
everything the worker does:
- The worker runs with its working directory set to an ephemeral git worktree
on a fresh
contextrelay/write/branch; ContextRelay captures only that worktree's diff. The worktree path is canonicalized and refused if it resolves into the primary tree, so a symlink or traversal cannot slip past. - ContextRelay never commits, merges, or pushes the worker's output. The
contained write worker always runs as Codex under
--sandbox workspace-write(a file-write sandbox that confines writes to the worktree and the system temp area, with no approvals bypass and no full-auto) - regardless of which agent's idle lane surfaced the opportunity. Your project - and its.contextrelay/config.json- lives outside those writable areas, so the worker cannot reach them; and as a fail-closed backstop, act:write refuses to run (falling back to read-only) if the project itself is inside a system-temp directory (os.tmpdir(),$TMPDIR,/tmp,/private/tmp). So a worker cannot write your primary tree or re-arm itself by editing.contextrelay/config.json. (Live-verified on macOS; the Codex sandbox is expected to behave the same on other platforms but is not yet separately verified.) (A Claude-run write worker is deferred until Claude has an equivalent OS sandbox; until then, if Codex is unavailable the write does not dispatch.) The worker leaves everything uncommitted; the daemon captures the diff and tears the worktree and branch down.
The result is recorded as an idle_write_result artifact - advisory until a
human or the git coordinator chooses to apply it.
Enabling act:write is covered step by step in
Enabling act:write (autonomous edits) safely.
The conceptual model lives in Read-only by default.
Explicit non-controls
Being honest about what ContextRelay does not do is as important as listing what it does. None of the items below are bugs - they are deliberate boundaries.
There is no OS sandbox around the agents themselves and no TLS. Claude,
Codex, tests, commands, and backup agents all run with your operator privileges.
The one exception is the contained act:write worker, which is launched inside
Codex's own --sandbox workspace-write OS sandbox (see the act:write
containment section above) - ContextRelay does not otherwise sandbox the
processes it coordinates. Loopback-only local traffic is the boundary.
ContextRelay trusts the underlying Claude Code and Codex processes; it cannot
defend against same-user local token theft or a malicious process running as your
user.
The single-git-owner policy is written into agent instructions and surfaced in state, but git permissions are not changed at the OS level. A non-coordinator agent is expected to hand off git work - nothing in the OS stops it. The policy is a convention the agents follow, backed by instructions and visibility, not a hard lock.
A native Claude Code Workflow runs sub-agents that can perform git, gh, or
merge operations directly - outside the ledger and outside the coordinator
policy. ContextRelay does not try to block this. Bypass mode (a repo that
intentionally drives Workflows directly) is a documented operator choice in which
ContextRelay is not the git owner and does not claim to be.
There is no cryptographic ledger integrity - local files can be edited by you or any same-user process. The browser viewer can clear current-session history through an authenticated local endpoint. Named-session ledger writes are tagged but not fully scoped in the current slice.
deliberate_with_codex, deliberate_with_claude, and /contextrelay:deliberate
bound an exchange and record the synthesis. They do not validate truth -
their output is advisory, and so is anything a read-only backup agent reports
(read-only is enforced by prompt, command choice, and workflow, not by an OS
sandbox).
Residual risks of act:write
Live testing of the contained writable worker surfaced two real limitations the
automated suites do not yet catch. Operators who enable act:write should know them:
- Diff capture can miss brand-new / untracked files. Capture is built on the
worktree diff, which can overlook files that are entirely new and untracked.
Treat the captured
idle_write_resultdiff as a starting point, not a guarantee of completeness. - Codex worker spend may not be parsed into the ledger. When a worker's cost
is not reported back, accounting falls back to the reserved estimate, so spend
tracking is best-effort. Watch the daily cap (
autonomy.writableAction.budgetUsd) rather than assuming per-task costs are always recorded precisely.
Because spend accounting is best-effort and the gate is the real backstop, keep
autonomy.writableAction.budgetUsd conservative. If the spend ledger becomes
unreadable, the writable worker fails closed until the daemon restarts - that is
the intended behavior, not a malfunction.
The human-authority boundary as a control
The strongest safety property is not a token or a sandbox - it is the rule that certain actions require a human, and that agents must escalate rather than act. Agents should not, on their own authority:
- Spend beyond the configured opt-in budgets.
- Publish, release, or take outward/destructive actions.
- Kill or restart daemons.
- Change the coordinator or the git-write policy.
These are reserved for a human (or, where policy and runtime permissions allow, the configured coordinator). Treat scanner suggestions and worker outputs as evidence to weigh, not instructions to obey.
How failures surface
ContextRelay's bridge and daemon detect common failure modes and latch a disabled state until things are healthy again, recovering only when the same instance and state directory check out.
| Symptom | What it usually means | First move |
|---|---|---|
| "daemon disconnected" | Often the turn watchdog (CONTEXTRELAY_TURN_MAX_MS, default 300000 ms) resetting a long turn - not a crash. | Check ctxrelay status before restarting; bump the watchdog for genuinely long turns. |
| Version skew | Bridge and daemon are on mismatched bundles. | The bridge latches disabled and recovers when versions match; reconcile with ctxrelay upgrade. |
| Stale session / lock / PID | A process or lock survived an abnormal shutdown. | Startup and kill paths probe before adopting or signaling; see the runbook if auto-recovery fails. |
| Foreign daemon collision | A port belongs to another project. | Identity checks reject reuse; stop the other project or use a different port group. |
| Token leak | A token file was read by something it shouldn't be. | Stop the daemon, delete the leaked token file, restart, and treat the old token as compromised. |
| Prompt injection via transcript | Hostile content in ledger entries, files, or messages. | Treat all agent-visible transcript as untrusted input when it is fed into prompts. |
A "daemon disconnected" message is the most-commonly-misread symptom. Inspect
state with ctxrelay status first - a watchdog reset of a long turn looks
identical to a crash but needs a different fix. See
Troubleshooting and recovery.
Where the authoritative detail lives
This page is a map. The full territory is in two documents shipped with the source tree:
docs/THREAT_MODEL.md- trust boundaries, the full controls table, non-controls,
act:writecontainment, and failure modes.
- trust boundaries, the full controls table, non-controls,
docs/CONTEXTRELAY_V1.md- runtime adapter model, permission policy, write-action boundaries, and security limits.
Next steps
- Read-only by default: safety and containment - the safety posture this threat model implements.
- Architecture overview - how the daemon, bridge, ledger, and adapters fit together.
- Enabling act:write (autonomous edits) safely - turn on contained writes the right way, with the residual risks above in mind.
- Coordinator and git-write policy - the single-git-owner convention and how to configure it.
- Environment variables reference - every
CONTEXTRELAY_*knob named on this page.