Skip to main content

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 the hardest thing in the product to turn on. This page is opinionated: it tells you exactly how to enable it, in what order, 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.

Default-off, fail-closed, and that is the point

In stock configuration - and with no environment variables set - nothing in the act:write path passes. A config file alone can never enable a write. 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 fully enabled and the idle scanner finds a concrete opportunity it is authorized to act on, the daemon dispatches a single bounded worker that:

  1. runs inside an ephemeral git worktree on a fresh contextrelay/write/ branch - never your primary tree;
  2. may edit files only inside that worktree, leaving changes uncommitted;
  3. is captured: the daemon reads the resulting diff (git diff) before teardown and records it as an idle_write_result artifact;
  4. 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.

Every gate, in order

act:write is gated by a fail-closed chain. Every gate must pass; the first failure stops the run with a distinct, explainable reason. The authorization gates are evaluated in this order (src/session/write-authorization.ts), and a separate per-day spend gate (src/session/idle-write-spend.ts) is AND-ed in by the daemon.

#GateWhat it requiresDefault
1Autonomy onctxrelay autonomy onoff
2Write mode = actautonomy.writableAction.mode is exactly "act" (set via ctxrelay write-mode act)"off"
3Positive per-task budgetautonomy.writableAction.budgets.costBudgetUsd > 00
4Hard env switchCONTEXTRELAY_WRITE_MODE_ENABLED set to 1/true/yes/on - no config can supply thisunset
5aKind allowlistauthorization.allowedKinds non-empty and includes this opportunity's kind[]
5bOwner allowlistauthorization.allowedOwners non-empty and includes this opportunity's owner[]
6Strict dual-idle quiescenceboth agents at strict idle (proven by the daemon)enforced
7Single-flightno other contained write already in progressenforced
8Containmentany configured worktreeRoot is outside the primary tree (and vice versa), checked on canonicalized pathsenforced
+Daily spend capCONTEXTRELAY_WRITE_DAILY_CAP_USD is a valid positive number, and spent + estimate ≤ cap for todayunset → fails closed

A few of these deserve emphasis:

  • Gate 4 is the one config can never satisfy. The hard env switch CONTEXTRELAY_WRITE_MODE_ENABLED lives only in your shell. This is the deliberate separation between "I edited a JSON file" and "I am, right now, authorizing autonomous writes in this terminal."
  • Empty allowlists authorize nothing. allowedKinds: [] and allowedOwners: [] are the defaults, and they mean nothing is permitted. You must name the specific opportunity kinds and owners you trust.
  • The daily cap's presence is the spend enable signal. Unlike read-only idle actions, there is no separate *_ENABLED flag for spend here. A missing or invalid CONTEXTRELAY_WRITE_DAILY_CAP_USD fails the worker closed. An absent spend ledger for the day simply counts as $0 spent so far.

The allowed opportunity kinds are the same ones the scanner emits - failed_check_no_followup, dirty_tree_finalizable, and claimed_complete_unverified - and the allowed owners are claude or codex.

The config surface

act:write is configured under autonomy.writableAction in .contextrelay/config.json. Every default is the closed/safe value:

{
"autonomy": {
"writableAction": {
"mode": "off",
"authorization": {
"allowedKinds": [],
"allowedOwners": []
},
"budgets": {
"tokenBudget": 0,
"costBudgetUsd": 0,
"dailyCapUsd": 0
},
"branchPrefix": "contextrelay/write/"
}
}
}

branchPrefix is the contextrelay/write/ prefix the ephemeral write-worktree branches use. worktreeRoot is optional and omitted by default (the worker uses an OS temp location); if you set it, gate 8 requires it to live outside your primary tree. See the config.json reference for the surrounding schema.

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. Set the config - the narrowest allowlist and smallest budget you can. Set the mode:

ctxrelay write-mode act

This sets autonomy.writableAction.mode to act and prints a warning that the config alone still cannot write. Then edit .contextrelay/config.json to add a single opportunity kind, a single owner, and small costBudgetUsd / tokenBudget per-task budgets. Start as small as is useful.

3. Turn on autonomy:

ctxrelay autonomy on

4. Set BOTH environment variables in the shell where the daemon runs. Neither has a default; both are required:

export CONTEXTRELAY_WRITE_MODE_ENABLED=1
export CONTEXTRELAY_WRITE_DAILY_CAP_USD=2.00

5. Verify the resolved state before expecting anything to happen:

ctxrelay write-mode status

This prints the current writeMode, the full writableAction config, and whether autonomy is enabled. Confirm the mode is act, the allowlists name what you expect, and the budgets are positive.

5b. Verify the gates. Run the gate table and let it tell you what is missing:

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, env export, or config key that makes it pass — there is no gate you have to reverse-engineer from source. 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.

6. 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.

Start narrow, widen slowly

Begin with one opportunity kind, one owner, a tiny per-task budget, and a low daily cap. Watch a few real runs end-to-end - dispatch, diff capture, your review - before you widen the allowlists or raise the cap. The gates are designed to make "too permissive" require deliberate effort.

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, which branches from your tree but is a separate directory. Containment is re-checked on canonicalized paths, so a symlinked base (for example macOS /tmp/private/tmp) cannot slip a worktree root past gate 8.
  • 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_result artifact.
  • 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 (gate 7), 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.

Known gaps in capture and spend accounting
  • 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 captured idle_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 CONTEXTRELAY_WRITE_DAILY_CAP_USD and the actual provider usage yourself rather than trusting the ledger to catch every dollar.

Because of these gaps, keep the daily cap low and the allowlists narrow until you have watched the feature behave on your own repository.

Hard rule

Never wire act:write on a shared or production repo without human awareness

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 do upgrade ContextRelay, your act:write settings are preserved: ctxrelay upgrade performs a config migrate-merge that adds new default keys without deleting your values - your mode, allowlists, budgets, and env-driven gates are untouched. See Upgrading ContextRelay.

Next steps