Operator coding worker
cw, in tools/coding-worker, supervises one Claude Code CLI child on an
existing Linux worker. A human supplies the task, inspects the evidence and
imports the result as a Git bundle. The worker does not open or merge PRs.
This is INF-576 slice 1: it provides no provisioning, remote installation or
teardown automation, and does not activate governed execution.
The workspace README
contains the full host layout, CLI options and integration seams. The examples
below assume cw invokes that workspace's bin/cw.js and is available under
sudo on the prepared host.
Operating conditions
Real use is limited to the personal envelope adopted by PLT-1398 on 2026-09-20:
- Use only the operator's own model subscription seat. Code running as the CLI user can read that token through the parent's process environment.
- Put no company or shared credentials on the worker: no Directory or tracker key, database URL, CI token, cloud role or instance profile. The agent user must be blocked from both instance metadata endpoints.
- Put no GitHub credential on the worker and never forward an SSH agent.
Use
ForwardAgent=nofor operator SSH connections. Transfer the repository and result as bundles. - Read the whole result diff before checking it out, running its gates or pushing it. Open the draft PR from your own machine.
- Use platform-tenant,
UNCLASSIFIEDtask content only; it reaches the model vendor. - Use one operator per instance.
This envelope is not evidence for governed admission. Dispatch, run-bound authority, credential brokering, model proxying and remote reporting remain separate integrations.
Host preparation
Use Node 20 or later, a pinned Claude Code CLI, an unprivileged agent account,
and a separate supervisor account owning the base repository. The default root
is /srv/coding-worker; override it with --root or CW_ROOT. Its base
directory contains the clone. Its secrets/seat-token file belongs to root and
has mode 0600 or 0400. Supply the operator's token directly on the host;
never commit it or put it in task text.
Run-scoped commands use root, then drop child processes to the agent's resolved
UID and primary GID. Preparation resolves both accounts before changing the
host; account names need not match group names. It requires working iptables
and ip6tables owner filters. A failed filter refuses preparation before any
repository code runs. Rejections are inserted first so an older acceptance rule
cannot shadow them; another preparation may add an equivalent rejection.
Ownership commands force numeric interpretation even if a numeric account or
group name exists. Both AWS metadata addresses are covered:
169.254.169.254 and fd00:ec2::254.
AWS metadata access documentation.
sudo cw prepare --run r1 --supervisor-user operator --warm-gates
sudo cw probe --run r1
Preparation creates a fresh checkout and bare local result remote, then runs
npm ci as the agent user. --warm-gates also runs the repository's gates.
A run ID is prepared once: an existing checkout, remote or run-meta.json
refuses reuse. Choose a new ID after an incomplete preparation.
The probe measures loaded context, then credential exposure. It reports IPv4
metadata, IPv6 metadata and unauthenticated model-API reachability separately;
response bodies are discarded. HTTP 000 means no response, not proof that a
firewall caused it. Inspect the installed filters too. A confirmation supplied
to probe applies only to already unknown attempts before its first launch.
If the context attempt ends with unknown usage, the credential attempt is
refused until the operator inspects and acknowledges that new outcome.
Both probe attempts have a fixed tool surface, whatever a task launch would
allow: the context attempt has no built-in tool at all, the credential attempt
only Bash with exactly its script command approved (script and root, no
wildcard), loading no settings file and no MCP servers. The context attempt
keeps the default settings sources because recording what the checkout loads
is its job, so the checkout's SessionStart hooks run in it as at every launch.
probe refuses --permission-mode and --allowed-tools.
Supervise a task
Write the task file yourself, then launch it:
sudo cw launch --run r1 --task-file /path/to/task.md
Defaults are a 45-minute deadline, 60 turns, $5 per-attempt budget, 300 tool
calls and a 30-second termination grace period. --run-budget-usd also checks
the run's accumulated known spend. Budget and turn figures come from the CLI;
the host enforces the deadline and observes the tool-call cap one call late.
Egress remains open apart from metadata filtering. Tasks requiring headless
writes to .claude/ are outside the tested worker capability: the CLI refuses
its own direct edits there, but a subprocess it runs is not constrained, so a
.claude/ change can still reach the bundle. Read the imported diff for one
before running anything.
Use another terminal for observation, steering or cancellation:
sudo cw tail --run r1
sudo cw steer --run r1 "Explain the failing test before changing the implementation."
sudo cw cancel --run r1
sudo cw report --run r1
sudo cw note --run r1 intervention "Inspected the interrupted attempt."
The steering code identifies operator messages to the model; it is not an authorization boundary. Root-only inbox access supplies the local control. Cancellation sends SIGTERM, waits the grace period, escalates to SIGKILL and checks for survivors.
Each attempt records events, stderr, a summary and lifecycle.json. A new
summary remains provisional until its matching lifecycle says finalization
finished. A failed final write cannot be trusted as completed or known usage.
Pending input writes settle before finalization or fail after five seconds with
unknown delivery recorded. Queued input is not proof of transport completion,
and transport completion is not model acknowledgement. Reports derive their data
from current evidence; run.json is a snapshot whose repair is attempted on failure
but can still lag if that repair fails. Unknown usage keeps any observed cost as a known part; it is never
treated as free execution. After inspecting all unknown attempts, acknowledge
them when starting the next one:
sudo cw launch --run r1 --task-file /path/to/task.md --confirm-unknown "Reviewed the failed attempt and its known spend."
Tail waits for evidence finalization, drains final bytes and prints a final unterminated line. It reports supervisor loss, failed finalization or an overdue writer as an error. Its recorded observation deadline includes the run deadline, grace, kill wait, sweep, stream drain and a 60-second finalization allowance. Legacy active records have a 50-minute observation bound. These bounds stop an observer waiting indefinitely; they never authorize killing or reusing a worker. Recovery reads raw evidence in 64 KiB chunks and refuses a serialized line above 64 MiB rather than silently dropping accounting evidence. Retained accounting observations still use memory proportional to their distinct entries.
Crashes and retained ownership
Preparation, launch and bundling lock the run, canonical workspace and, under
UID drop, the host-wide agent UID. Stale or unreadable locks are refused without
automatic takeover. A surviving child retains ownership even when finalization
has ended; lifecycle.json does not replace live.json or prove cleanup succeeded.
Inspect the named run, child process group and all processes of the agent UID before removing any stale lock or live record. Never remove locks while another operation is starting. Keep canonical directory paths stable during operations. A lifecycle still marked running after its observation deadline requires operator investigation, not automatic deletion or a fresh launch.
Review and import the result
The worker commits to worker/r1 and pushes to its bare local remote. Export
from that remote on the worker:
sudo cw bundle --run r1
Transfer the bundle using your operator-controlled connection and retain the printed SHA-256 and base commit. On your own machine:
cw import-bundle --bundle /path/to/result.bundle --repo /path/to/clone --sha256 <printed-digest> --base-commit <printed-base>
git -C /path/to/clone diff <printed-base>..worker/r1
Import validates the bundle and prints commits plus a tree diffstat without
checking out or pushing the branch. Rejected imports remove the new branch ref
with checked rollback; fetched objects may remain as Git data. Read the entire
tree diff, including merge resolutions. Only then check out the branch, run
npm ci and npm run prepush, push and open a draft PR. The worker's own local
push does not prove its pre-push hooks ran. Your checkout and gates execute
worker-authored code with your credentials, and a push can start CI and previews.
Remove the instance using the provisioning system that created it when finished;
cw cannot tear it down.