Enabling act:write (autonomous edits) safely
act:write is the only path in ContextRelay where an agent edits files on its own. It is a ContextRelay 3.x capability, it is off by default, and it is deliberately conservative. This page tells you exactly how to enable it and where it can still surprise you.
Everywhere else, ContextRelay is read-only by default. Agents reason, message, hand off, and propose - but they do not change your working tree unless a human or the coordinator applies a change. act:write is the narrow, budgeted, supervised exception. Treat it as one.
In stock configuration, nothing in the act:write path passes: enabled is false and budgetUsd is 0. If you do nothing on this page, no agent can ever edit your files autonomously. Keep it that way unless you have a concrete, contained reason not to.
What act:write actually is
When act:write is armed and the idle scanner finds a concrete opportunity it is authorized to act on, the daemon dispatches a single bounded worker that:
- runs inside an ephemeral git worktree on a fresh
contextrelay/write/branch - never your primary tree; - runs inside Codex's
workspace-writeOS sandbox, which confines its writes to that worktree and the system temp area; 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); - may edit files only inside that worktree, leaving changes uncommitted;
- is captured: the daemon reads the resulting diff (
git diff) before teardown and records it as anidle_write_resultartifact; - is then torn down: the worktree and its throwaway branch are removed.
The worker never commits, never merges, and never pushes. Its entire output is a diff artifact you can review. Applying that diff is a separate, explicit decision made by a human or the coordinator - it is not part of the autonomous run.
That is the core safety model: autonomy produces evidence (a proposed diff), not changes to your repo - and because the OS sandbox confines writes to the worktree and the system temp area (both of which your project lives outside), and act:write additionally refuses to run if the project is itself under a system-temp directory, the worker cannot escalate its own permissions or re-arm itself. (Live-verified on macOS; the Codex sandbox is expected to behave the same on other platforms but is not yet separately verified.)
The two-knob surface
act:write is configured by exactly two fields under autonomy.writableAction in .contextrelay/config.json. Both default to the closed/safe value:
{
"autonomy": {
"writableAction": {
"enabled": false,
"budgetUsd": 0
}
}
}
enabled- the single arming switch. Defaultfalse.budgetUsd- the daily USD spend cap for contained writes. Default0. Must be> 0to arm.
act:write is armed only when enabled === true and budgetUsd > 0. That is the entire public surface. Everything else is a non-user-facing safety floor (see below) - there is no separate mode, no allowlist to configure, and no environment variable to export.
These two knobs only matter while the global master switch autonomy.enabled is on. act:write is gated by autonomy.enabled in addition to its own surface, so ctxrelay autonomy off disables act:write regardless of enabled/budgetUsd. Enable in this order: ctxrelay autonomy on, then ctxrelay act on --budget <usd>.
Earlier releases used a larger surface - mode, authorization.allowedKinds/allowedOwners, separate per-task/token/daily budgets, and the CONTEXTRELAY_WRITE_MODE_ENABLED / CONTEXTRELAY_WRITE_DAILY_CAP_USD environment variables. Those are gone. The env vars are removed entirely, and a legacy writableAction block is migrated to { enabled, budgetUsd } automatically on config load (a legacy mode: "act" becomes enabled: true; the old daily/per-task budgets seed budgetUsd). ctxrelay upgrade preserves your values, but the legacy→{ enabled, budgetUsd } normalization runs on any config load, not only during upgrade.
The hard internal floor (not configurable)
Arming only makes act:write eligible. Every run is still bounded by a fail-closed floor you cannot turn off - the first failure stops the run with a distinct, explainable reason. These are the enforced floors; ctxrelay act status prints the core subset as a hardFloors array, but the table below lists every floor the run is held to (a few, such as the temp-primary refusal and the global-autonomy prerequisite, are enforced without appearing verbatim in that JSON):
| Floor | Guarantee |
|---|---|
| Global autonomy on | autonomy.enabled must be on (ctxrelay autonomy on); ctxrelay autonomy off disables act:write entirely. |
| Dirty-tree only | Only the dirty_tree_finalizable opportunity kind may write. No other kind is authorized. |
| Trusted owners only | Only claude- or codex-owned lanes may trigger a write. |
| Codex-sandboxed worker | The worker always runs in Codex's workspace-write sandbox; writes are confined to the worktree + system temp, which your project lives outside. |
| Primary-not-under-temp | act:write refuses to run (falls back to read-only) if the primary repo is under a system-temp directory (os.tmpdir(), $TMPDIR, /tmp, /private/tmp), closing the case where the project lives where the sandbox can write. |
| Strict dual-idle | Both agents must be at strict idle when dispatch happens. |
| Single-flight | Only one contained write may be in progress at a time. |
| Daily budget | spent + per-task estimate ≤ budgetUsd for today; a missing/invalid budget fails closed. |
| Capture before teardown | The diff is recorded as an artifact before the worktree is removed; the worker never commits/pushes. |
The per-task cost estimate is derived internally (the lesser of $1 and your budgetUsd); you do not set it. You can see the resolved surface and the enforced floors at any time:
ctxrelay act status
This prints the current enabled and budgetUsd, the full writableAction block, and a hardFloors array covering the core floors above.
Regardless of which agent's idle lane surfaced the opportunity, the contained write worker runs through Codex's workspace-write sandbox (the opportunity owner is kept only for ledger attribution). That sandbox confines writes to the worktree and the system temp area; your project and its .contextrelay/config.json live outside both, and act:write refuses to run if the project is itself under a system-temp directory - so a worker cannot edit your primary tree or re-arm itself. Claude-as-a-write-worker is deferred until it has an equivalent OS-level file sandbox; until then, if Codex is unavailable the write simply does not dispatch - it falls back to read-only. This is what makes "a worker cannot re-arm itself" a structural guarantee rather than a prompt instruction. (Live-verified on macOS; the Codex sandbox is expected to behave the same on other platforms but is not yet separately verified.)
Recommended enablement order
Do these in sequence. Stop and reconsider at any step you are not sure about.
1. Read the safety model first. This page and Read-only by default. If act:write does not feel justified for the repo in front of you, it is not - leave it off.
2. Turn on global autonomy - act:write is gated by the master switch, so it can never dispatch while autonomy is off:
ctxrelay autonomy on
3. Arm it with the smallest useful cap - a single command sets both knobs:
ctxrelay act on --budget 1.00
This sets autonomy.writableAction.enabled = true and autonomy.writableAction.budgetUsd = 1.00, then prints the resolved gate state. Start as small as is useful. (ctxrelay act off disarms; ctxrelay act --budget <usd> adjusts the cap without changing the switch. Later running ctxrelay autonomy off also disables act:write without touching these two knobs.)
4. Verify the resolved state and the gates before expecting anything to happen:
ctxrelay act status # resolved enabled/budgetUsd + the enforced floors
ctxrelay idle-scanner check # exits non-zero when act cannot dispatch
ctxrelay idle-scanner status --why # the same gate rows as JSON, plus daemon diagnostics
Every failing gate prints a ↳ satisfy: line naming the exact command or config key that makes it pass - there is no gate you have to reverse-engineer from source. If global autonomy is off, this is where it shows up: turn it back on with ctxrelay autonomy on. Once the daemon is running, status --why also reports why the last dispatch did not happen (lastIdleDispatchSkip) and the last opportunity the scanner detected (lastIdleOpportunity), so "act is on but nothing happened" is always explainable.
5. Review the diff before applying anything. When a write runs, its output is an idle_write_result artifact - a proposed diff, not a change to your tree. Read it (the browser Command Deck surfaces artifacts) and decide deliberately whether to apply it. Applying is a separate human/coordinator action.
Begin with a low daily cap and watch a few real runs end-to-end - dispatch, diff capture, your review - before you raise the cap. The floor is designed so that "too permissive" is simply not reachable: only dirty-tree finalization, only at strict idle, only one at a time, only inside a sandbox.
Containment guarantees
These hold by construction, not by good behavior:
- Writes never touch the primary tree. Edits happen only inside the ephemeral
contextrelay/write/worktree - a separate directory. The worker runs in Codex'sworkspace-writesandbox, which confines writes to the worktree and the system temp area; your project lives outside both, so the OS blocks writes to it - 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). (Live-verified on macOS; the Codex sandbox is expected to behave the same on other platforms but is not yet separately verified.) - The worker cannot re-arm itself. Your primary
.contextrelay/config.jsonlives outside the sandbox-writable areas (worktree + system temp), and act:write refuses if the project is under system temp, so the worker cannot edit it to flipenabledor raisebudgetUsd. Arming stays a human decision. - The output is a diff, not a commit. The worker leaves changes uncommitted; the daemon captures the diff before removing the worktree and records an
idle_write_resultartifact. - Applying the diff is a separate decision. Nothing in the autonomous path commits, merges, or pushes. A human or the coordinator applies the change - or does not.
- It is single-flight. Only one contained write can be in progress at a time, so concurrent writers cannot race.
Current limitations - read before you rely on it
act:write is contained, but the evidence it produces is best-effort. These are honest gaps observed in live testing; the containment guarantees above still hold.
- Brand-new files can be missing from the captured diff. Diff capture is based on
git diff. Untracked files the worker creates inside the worktree may not appear in the capturedidle_write_result. If an opportunity is expected to add new files, do not assume the artifact is a complete record of what the worker did. - Spend tracking may not append for some runs. Codex worker cost/token usage is not always parsed, so the per-day spend ledger may not record those runs. Treat spend tracking as best-effort: watch your
budgetUsdcap and the actual provider usage yourself rather than trusting the ledger to catch every dollar. - Writes currently run as Codex only. Claude-owned opportunities are eligible, but the contained worker that executes them is always the Codex sandbox. If Codex is unavailable, the write does not dispatch.
Because of these gaps, keep the daily cap low until you have watched the feature behave on your own repository.
Hard rule
act:write is for contained, budgeted, supervised experiments - not for unattended work on code that other people depend on. Do not enable it on a shared or production repository unless a human is aware it is on and is reviewing the diffs. If you are unsure whether a repo qualifies, it does not.
When you upgrade ContextRelay, your act:write settings are preserved: ctxrelay upgrade performs a config migrate-merge that adds new default keys without deleting your values. The legacy writableAction block is normalized to the { enabled, budgetUsd } surface on config load (so the migration applies even outside upgrade). See Upgrading ContextRelay.
Next steps
- Read-only by default: safety and containment - the model
act:writeis the exception to. - config.json reference - the full
autonomy.writableActionschema and surrounding keys. - Autonomy, idle scanner, and safe automation - how the scanner finds opportunities and dispatches read-only workers.