amelnagdy/delegate-skills実行前に内容を確認
SKILL DETAIL
claude-delegate
amelnagdy/delegate-skills/claude-delegate
>-
インストール · 121出典を見る
Installation
npx skills add https://github.com/amelnagdy/delegate-skills --skill claude-delegate
スキルファイル
SKILL.md
最終同期 · 2026/08/30
references/dispatch-and-poll.md›
# Dispatch and poll
`scripts/relay.mjs` wraps Claude Code's non-interactive `claude -p` mode, sends a brief on stdin,
captures the structured stream, and writes `delegate-relay.result.v1`.
## Before the first run
```bash
command -v claude
claude --version
claude auth status
```
The relay performs its own `claude --version` preflight and records the answer as `claudeVersion`.
When an orchestrator is itself Claude Code, the relay removes only inherited `CLAUDECODE` from the
child environment so a separate CLI process can start. It preserves credentials,
`CLAUDE_CODE_CHILD_SESSION`, and every other environment entry. Version preflight uses that same
environment.
## Dispatch
```bash
node "<skill-dir>/scripts/relay.mjs" --brief brief.txt --cd /path/to/repo
```
`<skill-dir>` is the installed folder containing this skill's `SKILL.md`.
| Flag | Effect |
| --- | --- |
| `--brief <file>` | Brief path. Omit it to read stdin. The exact text is then sent to Claude on stdin, never argv. |
| `--cd <dir>` | Child process cwd and target working root (default: current directory). |
| `--lane <name>` | Fleet lane from `delegate-setup` config. Applies that lane's dials; fails if the lane's `implementer` is not this relay. Explicit dial flags win. |
| `--out-dir <dir>` | Artifact directory (default: a fresh directory under the system temp directory). |
| `--timeout <dur>` | Relay watchdog, such as `30m`, `90s`, or `2h` (default: off). |
| `--model <name>` | Claude model alias or full name (default: Claude's configured choice). The relay does not pin a model version. |
| `--effort <level>` | Claude effort: `low`, `medium`, `high`, `xhigh`, `max`, or `ultracode`; availability depends on the model. |
| `--max-turns <n>` | Positive agentic-turn cap. |
| `--max-budget-usd <amount>` | Positive decimal spend cap for print mode. |
| `--resume-last` | Resume the latest session for this cwd with Claude's `--continue`; send a delta brief. |
| `--session <id>` | Resume a specific session with Claude's `--resume <id>`; mutually exclusive with `--resume-last`. |
| `--read-only` | Plan mode with only Read, Glob, and Grep, plus a Git-visible change tripwire. |
| `--dangerously-skip-permissions` | Opt into Claude's `bypassPermissions`; mutually exclusive with `--read-only`. |
| `-h`, `--help` | Print the relay header and option reference. |
Values that can reach an npm `claude.cmd` launch on Windows are token- or number-validated. The brief
never reaches a shell command line.
## What the relay launches
The common shape is:
```bash
claude -p --output-format stream-json --verbose \
--tools Read,Glob,Grep,Edit,Write,Bash \
--strict-mcp-config --disallowedTools 'mcp__*' \
--disable-slash-commands \
--settings <profile.json> \
--permission-mode acceptEdits \
< brief.txt
```
On native Windows, `PowerShell` replaces `Bash` and is passed through `--allowedTools` because Claude's
shell sandbox is unavailable there. Read-only uses `--tools Read,Glob,Grep --permission-mode plan`. A
specific session adds `--resume <id>`; the latest session adds `--continue`. The permission and tool
profile is re-passed on every resumed invocation.
The relay never adds `--bg`: Claude documents background mode as incompatible with `-p`. It never adds
`--bare`, because bare mode skips `CLAUDE.md` and OAuth/keychain authentication.
`--strict-mcp-config` without an MCP config prevents configured-server discovery. The inline settings
also disable Claude.ai connectors, while `--disallowedTools 'mcp__*'` denies any MCP tool that managed
policy still supplies. `--disable-slash-commands` prevents skill and command recursion, and `--tools`
omits the Agent tool. These controls do not suppress project `CLAUDE.md` or normal authentication.
## Permission reach
### Normal write-capable profile
The normal profile pairs `acceptEdits` with sandbox auto-approval on supported platforms.
`acceptEdits` accepts file edits but does not, by itself, approve ordinary shell gates in a headless
run. `autoAllowBashIfSandboxed: true` approves commands that stay inside Claude's sandbox. A command
that cannot stay sandboxed fails instead of being retried outside it. Native Windows is the explicit
exception: the relay pre-approves PowerShell because no Claude shell sandbox is available there.
The generated `profile.json`:
- uses string rules to deny common direct shell forms of `git commit`, `git push`, and nested
`claude`, plus any command containing `claude-delegate`; aliases, scripts, and wrappers can bypass
these speed bumps, so the brief's no-commit instruction and orchestrator review remain the boundary;
- on macOS, Linux, and WSL2, enables Claude's Bash sandbox with `failIfUnavailable: true`,
`autoAllowBashIfSandboxed: true`, `allowUnsandboxedCommands: false`, and filesystem isolation
explicitly enabled;
- on native Windows, leaves the unsupported sandbox unconfigured and enables Claude's PowerShell
tool through its documented settings environment switch;
- disables Claude.ai connectors for every profile.
On supported platforms, the strict settings prevent a shell command from silently falling back to an
unsandboxed retry. Claude's sandbox covers **Bash and its child processes only**. Edit/Write remain
Claude Code tools governed by its permission system; the Claude process, local hooks, inherited
configuration, and unrelated host processes are not enclosed in a universal workspace sandbox.
Claude merges some sandbox arrays across settings scopes. Existing managed, user, project, or local
allowlists and `excludedCommands` can therefore affect the effective shell boundary, while managed
policy can further restrict or reject the run. Existing `ask` and `deny` permission rules take
precedence over sandbox auto-approval, so they can still stop a headless gate. Inspect both
`profile.json` and the effective local Claude configuration when the precise boundary matters. Use a
container or VM when only a host-level boundary is acceptable.
### Read-only profile
`--read-only` removes Edit, Write, Bash/PowerShell, Agent, MCP, skills, and commands from the child and
uses `plan` mode. Local hooks still load outside that tool surface and can write. The relay compares
parsed `git status --porcelain -z -uall` and fingerprints working-tree identity and index entries for
paths that were already dirty on every outcome, including aborts:
- `readOnlyViolation: true` — either signal proves a Git-visible change.
- `readOnlyViolation: false` — coverage was complete and neither signal detected a change.
- `readOnlyViolation: null` — coverage was incomplete, for example because git could not report or a
dirty submodule or unreadable path could not be fingerprinted.
This detects new dirt and changes to readable, already-dirty Git-visible paths, but cannot attribute a
concurrent change. Ignored paths, submodule internals, and writes perfectly restored before the final
snapshot remain outside coverage. Inspect the diff whenever read-only integrity matters.
### Permission bypass
`--dangerously-skip-permissions` passes Claude's flag and records
`permissionMode: "bypassPermissions"` unless the init event reports another value. The explicit tool
surface, commit/push deny rules, MCP/skill restrictions, and supported-platform shell sandbox remain.
However, direct file tools can cross ordinary Claude permission boundaries. This mode requires the
human's explicit acceptance.
## Artifacts
The default artifact directory is outside the repository so relay output does not pollute
`touchedFiles`. A caller-selected `--out-dir` inside the worktree will appear in git status. For a
meaningful `--read-only` review, keep artifacts outside the worktree. The fingerprint signal excludes
only the relay-owned artifact paths, so their later writes do not prove a violation.
- `brief.txt` — exact stdin brief.
- `events.jsonl` — raw stdout bytes from `--output-format stream-json`.
- `final.txt` — final `result` event's `result` text; present even when empty.
- `stderr.txt` — complete stderr.
- `profile.json` — exact inline settings passed to Claude.
- `result.json` — stable result contract, written atomically.
## `result.json`
Core fields:
- `schema` — `"delegate-relay.result.v1"`.
- `tool` — `"claude"`.
- `status` — `completed` | `failed` | `timeout` | `aborted` | `claude_unavailable`.
- `exitCode` — Claude's code, `127` when missing, a signal-derived code when available, or a forced
non-zero value when the watchdog or terminal error result makes a zero code non-successful.
- `signal` — terminating child/relay signal when reported, otherwise `null`.
- `claudeVersion` — version preflight text, `"unknown"` when the binary answered abnormally, or `null`
when unavailable.
- `permissionMode` — selected profile, updated from `system/init` when present.
- `sessionId` — parsed defensively from init/result events; use with `--session`.
- `resultSubtype` — final result subtype, such as `success` or an error subtype.
- `finalMessage` — final result text.
- `numTurns`, `usage`, `totalCostUsd` — terminal result metadata when present.
- `touchedFiles` — final `git status --porcelain` lines for `--cd`; `null` means git could not report,
while `[]` means git reported a clean tree. This is the whole final tree, not attribution.
- `readOnlyViolation` — present only on `--read-only`, with the three-state meaning above.
Run metadata includes `workdir`, `model`, `effort`, `maxTurns`, `maxBudgetUsd`, `timeout`, `readOnly`,
`resumed`, `resumeLast`, `toolSurface`, `shellSandbox`, `dangerouslySkipPermissions`, timestamps, and
all artifact paths. Failed, timed-out, and aborted runs include `stderrTail` when available;
launch/watchdog/signal failures include `error`.
The relay prints a concise summary and the complete final report to stdout, then exits with
`result.json`'s `exitCode`.
## Wait for completion
The relay blocks. Use the orchestrator's background-command facility or foreground it for short work.
If using a shell's own background feature, completion requires both:
1. the relay process has exited; and
2. `result.json` contains a terminal `status`.
A usage error exits 2 before creating `result.json`, so also observe process exit. A missing CLI is
different: it exits 127 **with** `status: "claude_unavailable"`.
## Failure recovery
- **`claude_unavailable`:** install Claude Code, authenticate with `claude auth login`, and verify the
same PATH the orchestrator uses.
- **`failed`:** inspect `resultSubtype`, `error`, `stderrTail`, `stderr.txt`, and the tail of
`events.jsonl`. Common causes are authentication, a model/effort mismatch, managed policy, a missing
sandbox dependency, or a gate that needs access outside the strict shell boundary.
- **`timeout`:** the relay sent termination to the whole process group/tree and escalated. The working
tree may contain partial edits; inspect it before resuming or re-dispatching.
- **`aborted`:** the relay caught SIGTERM, SIGINT, or SIGHUP, terminated the implementer tree, wrote an
outcome, then refreshed `touchedFiles` after a grace window. Native Windows cannot deliver every
termination as a catchable Node signal; a vanished relay with no result still requires direct
artifact/tree inspection.
- **Host `SIGKILL`:** the relay cannot catch its own SIGKILL. If the child reports SIGKILL, investigate
host memory or supervisor deadlines.
- **Empty `finalMessage`:** inspect the raw events and diff. Require a closing report in the next brief.
Never clean, reset, or switch branches before inspecting partial work, staged changes, and untracked
files.
## Windows launch
The relay resolves PATH itself. A native `claude.exe` is spawned directly. An npm `claude.cmd` is
invoked through `cmd.exe /d /v:off /s /c` with every argument quoted and user-selectable values
restricted; stdin still carries the brief. `taskkill /t /f` terminates the process tree.
This path is implemented but not yet verified on native Windows. Claude's Bash sandbox is unsupported
there, so even a successful Windows smoke would verify launch and termination mechanics, not provide
the supported-platform shell boundary.
## Commit boundary
The relay never commits. Claude edits; the orchestrator reviews, re-runs gates, and lands. See
[review-and-land.md](review-and-land.md).
references/multi-task-queues.md›
# Multi-task queues
The single-task loop scales to a migration, removal, or refactor queue. Sequencing and bookkeeping,
not parallelism, keep the work reviewable.
## Run sequentially
Dispatch one task at a time in dependency order. Review, run gates, and land it before dispatching the
next:
```bash
node "<skill-dir>/scripts/relay.mjs" --brief task-01.txt --cd /path/to/repo
```
- Later tasks can rely on earlier behavior only after it lands.
- One reviewed commit per task keeps rollback and history clear.
- A clean tree before each dispatch makes `touchedFiles` useful.
Use parallel dispatches only for genuinely independent tasks in separate working trees. Multiple
implementers editing one tree destroy attribution and make the review boundary unreliable.
## Use fresh sessions for fresh tasks
Each unrelated queue item should start a new Claude session. Use `--resume-last` or `--session <id>`
only for rework on the same task; send a delta brief.
A fresh session does not remember prior queue decisions. If task 2 chose a helper name, fixture
location, interface, or migration ordering that task 5 needs, write that fact explicitly into task
5's brief.
Claude Code will discover `CLAUDE.md`, but it does not generically auto-load `AGENTS.md`. Carry the
applicable `AGENTS.md` constraints into every brief rather than assuming the first session established
them for later sessions.
## Keep a progress file
For more than two or three tasks, maintain one durable progress file:
- **Status:** queued / dispatched / reviewed+landed, including the commit hash.
- **Per-task review:** what landed, what was inspected, and gate outcomes.
- **Needs your eyes:** design decisions, non-blocking concerns, and questions for the human.
- **Session/artifact pointers:** the task's `sessionId` and `result.json` path for rework or diagnosis.
- **End-of-run gates:** the final cross-task verification still required.
Update it when each task lands, not in one batch at the end.
## Close with coherence
After the last task:
- run the full project gates, not only the last task's narrow slice;
- search repository-wide for the concept the queue migrated, removed, or renamed;
- replay all new migrations from a clean state and check drift when applicable;
- inspect the final commit sequence and working tree;
- only then push and open or update the pull request.
## Stop and ask when
Proceed on work that follows from the agreed queue. Stop and surface when:
- a task cannot be completed correctly within its brief;
- review calls the plan itself into question;
- a gate reveals a problem in an already-landed task;
- the next task requires a permission or host-boundary change the human did not approve.
Report what has landed, commit hashes, the current tree state, and the open question, then wait.
references/review-and-land.md›
# Review and land
The separate Claude session did the typing; the orchestrator owns the judgment. Verify against the
working tree and gate output, never against the implementer's self-report.
## Review tests before trusting gates
If existing tests changed, inspect those edits first:
- An unbriefed test edit is a contract change, not automatically part of the fix.
- Treat a new skip, disable marker, commented-out case, or deleted test as a failure until justified.
- Reject assertions weakened from exact behavior to contains/truthy, broader error types, or wider
tolerances unless the brief required that semantic change.
A green gate proves less if the implementer shortened the yardstick.
## Re-run the gates
`finalMessage` reports Claude's claims. Run the project's actual test, lint, format, type, and build
commands yourself in the final working tree and read their output. Passing is necessary, not
sufficient.
Add verification suited to the change:
- **Migrations/schema:** apply, reverse, and re-apply from a clean scratch state; check drift.
- **Removals/renames:** search repository-wide for dangling names and stale docs/config.
- **Stateful behavior:** exercise the behavior, not only compilation.
- **Generated output:** regenerate it through the canonical command and compare.
## Inspect the complete tree
Start with `touchedFiles`, but remember it is final git porcelain, not attribution. It includes
pre-existing dirt and can omit modifications inside ignored files. Inspect:
```bash
git status --short
git diff
git diff --cached
```
Open every untracked file directly; ordinary `git diff` does not show its contents. Review staged
changes even though the relay denies common direct `git commit`/`git push` shell forms and the brief
forbids staging. A local hook, another tool, or an unusual command path may still have touched the
index.
For `--read-only`, treat `readOnlyViolation: true` as a hard warning and `null` as unknown. `false`
means the Git-visible tripwire had complete coverage and detected no change. Ignored paths, submodule
internals, perfect restores, and attribution remain outside its contract. Compare the actual diff when
read-only integrity matters.
## Hold the diff against the brief
- **Scope creep:** files or behavior the brief excluded, unrelated cleanup, opportunistic renames.
- **Scope shortfall:** missing edge cases, integration updates, cleanup, or required gates.
- **Quiet judgment calls:** defensible choices not authorized by the brief. Understand and surface
them rather than silently accepting them.
- **Repository constraints:** especially constraints copied from `AGENTS.md`, which Claude Code does
not generically auto-load.
## Implementer sweep
Generated code can satisfy tests while remaining wrong. Check every diff for:
- hardcoded success, fixture values, or fake fallbacks on real-work paths;
- broad catches that suppress failures and return defaults;
- APIs, methods, flags, and dependencies absent from the installed versions;
- unused imports, uncalled helpers, unreachable branches, and scaffolding comments;
- a second HTTP client, error idiom, state mechanism, or logging style beside the existing one;
- tests that assert implementation details or mock the project's own behavior;
- near-duplicate tests that inflate volume without adding behavior coverage;
- optional parameters, configuration, or abstractions with no caller;
- guards for impossible internal states that obscure real trust-boundary validation;
- network or filesystem assumptions hidden by the implementer's environment.
Run relevant guard skills when installed. Anything blocking goes back through a delta brief or is
fixed in the tree, and either choice is reported to the human.
## Preserve interrupted work
From dispatch until a reviewed commit, the uncommitted tree is the authoritative copy. Do not
reflexively run `git checkout`, `git reset`, `git clean`, or switch branches after a timeout, abort, or
failed result. First inspect status, unstaged and staged diffs, untracked files, `events.jsonl`, and
`stderr.txt`. After inspection, discarding premise-invalid work can be the correct decision.
## Rework in the same session
Send only the review delta:
```bash
echo "The runtime fix is correct. Replace the mocked database test with the existing migrated fixture,
remove the unused import, rerun the original gates, and leave the tree uncommitted." |
node "<skill-dir>/scripts/relay.mjs" --session <id> --cd /path/to/repo
```
Use `--resume-last` only when the latest session for that cwd is unambiguous. `--session <id>` is safer
when several Claude sessions exist. The relay maps them to `--continue` and `--resume`, respectively,
and re-passes the permission profile.
Rework gets the same test review, gate rerun, diff review, and implementer sweep. Repeat until the work
holds.
## Commit boundary
When the gates pass and the diff satisfies the brief, **the orchestrator commits**, never the
implementer. Write a clear message describing what landed.
## Surface, do not absorb
The human opted into delegation, so landing verified work is the contract. Keep them informed when
the work changes shape:
- report design decisions and defensible-but-unrequested turns;
- note non-blocking concerns you chose not to block on;
- stop and ask when correct completion requires expanding the brief.
For a queue, record these in the progress file described in
[multi-task-queues.md](multi-task-queues.md).
references/writing-the-brief.md›
# Writing the brief
A brief carries the task-specific context from the orchestrator to the separate Claude Code session.
The implementer has **no orchestrator chat history or other shared context**. It receives the brief on
stdin, can inspect the target working tree, and loads Claude Code's usual local context as described
below. A resumed session also retains its own Claude conversation.
If a fact is not in the brief, discoverable in that tree, or present in the loaded Claude context, do
not assume the implementer knows it.
## Know what Claude loads
The relay deliberately does not use `--bare`, so Claude Code discovers the project's `CLAUDE.md`,
normal local Claude settings, and session state in its usual way. The relay overrides a small,
inspectable subset for the child: MCP servers are not discovered, skills and commands are disabled,
and the built-in tool surface is restricted. Local hooks still load; account for any repository
effects they are configured to perform.
Claude Code does **not** generically auto-load `AGENTS.md`. Before writing the brief:
1. Read the applicable `AGENTS.md` files yourself.
2. Copy every load-bearing rule into the brief: scope boundaries, forbidden patterns, required
commands, generated-file policy, and commit policy.
3. Name the real gates rather than telling Claude to "run the tests."
The implementer can read an `AGENTS.md` when the brief points to it, but that is explicit task context,
not automatic Claude Code behavior.
## A compact structure
Use a bounded, block-structured brief:
```xml
<task>
State the concrete job, current behavior, desired behavior, and where it lives. Name what must remain
untouched. Include any facts from the orchestrator conversation that the implementer cannot discover
from the tree.
</task>
<repo_constraints>
Copy the applicable load-bearing constraints from AGENTS.md and other project instructions here.
Claude also loads CLAUDE.md, but restate rules whose violation would invalidate the work.
</repo_constraints>
<verification_loop>
Run these exact project gates, fix failures caused by the change, and report the final outcomes:
<actual test command>
<actual lint/format command>
<actual build/typecheck command>
Confirm the working tree contains only intended changes.
</verification_loop>
<action_safety>
Keep changes within the task. Do not perform unrelated cleanup. Do not run git add, git commit, or git
push. Do not invoke another Claude session or delegation skill. Leave all work uncommitted for the
orchestrator to review and land.
</action_safety>
<structured_output_contract>
End with:
1. What changed and why
2. Files touched
3. Gate outcomes, including useful counts
4. Deviations, open questions, and decisions the orchestrator should review
</structured_output_contract>
```
Remove empty blocks rather than adding ceremony. Add focused blocks when needed:
- **Debugging:** `<completeness_contract>` to require a full root-cause fix, and
`<missing_context_gating>` to prohibit guesses about missing repository facts.
- **Read-only diagnosis:** `<grounding_rules>` to require file/line or command evidence and clearly
label inference. Dispatch with `--read-only`.
- **Migration or removal:** an explicit repository-wide search and round-trip requirement.
## Discover the real gates
Read the repository's `CLAUDE.md`, `AGENTS.md`, `Makefile`, package scripts, and language tooling before
dispatch. Copy exact commands into `<verification_loop>`. Include required setup and the narrowest
useful test slice, but do not replace a required full gate with a guessed shortcut.
`acceptEdits` alone does not approve ordinary gate commands in non-interactive mode. On supported
platforms the normal relay profile auto-approves commands that stay inside Claude's shell sandbox and
requests failure when that sandbox is unavailable. A gate that needs network access, host services,
or writes outside the working tree may fail under that profile; state the need in the brief and decide
whether a different isolated environment is appropriate instead of silently weakening the boundary.
Merged local or managed settings can affect the effective sandbox. Native Windows pre-approves
PowerShell without that sandbox; see [dispatch-and-poll.md](dispatch-and-poll.md).
## One task per brief
One brief → one separate Claude session → one reviewed commit keeps scope and rollback clear. Split a
mixed request such as "fix the bug, redesign the API, update unrelated docs, and propose a roadmap"
into separate dispatches.
Use a resumed session only for rework on the same task. Start unrelated queue items in fresh sessions.
## Premises freeze at dispatch
There is no steering channel while the relay is running. Audit ownership, scope, branch, constraints,
and expected behavior before dispatch. If a premise changes during the run, stop it and inspect the
working tree before sending a corrected brief. Do not discard partial edits before reviewing them.
## Delta briefs for resumed sessions
`--resume-last` maps to Claude's `--continue`; `--session <id>` maps to `--resume <id>`. Both retain the
conversation, so send only what changed:
```xml
<review_delta>
The implementation behavior is correct. Replace the test's mocked database session with the existing
migrated fixture, remove the unused import, run the same gates, and leave the tree uncommitted.
</review_delta>
```
The relay re-passes the selected permission profile on resume. A resumed run receives the same review
as a fresh run.
## Brief delivery
The relay reads `--brief <file>` or stdin, saves the exact text as `brief.txt`, and sends it to
`claude -p` through stdin — never as an argv value. It therefore stays out of the process list and
needs no shell quoting. Claude Code caps piped stdin at 10 MB; the relay rejects a larger brief before
dispatch. Put large context in workspace files and reference those paths instead.
## Worked example
```xml
<task>
In services/billing/, refund retries can create a second refund because the idempotency key is checked
after submission. Check for an existing refund before creating one. Touch only refund handling and
its behavior-level tests. Leave charge creation, routes, and data models unchanged.
</task>
<repo_constraints>
Follow the repository's Python style and test conventions copied from AGENTS.md. Do not add ticket
identifiers to source comments. Do not add dependencies.
</repo_constraints>
<verification_loop>
Run and make green:
pytest tests/billing/ -q
ruff check services/billing/ tests/billing/
Confirm git status contains only the intended refund implementation and tests.
</verification_loop>
<action_safety>
No unrelated refactors. Do not git add, commit, or push; leave the work uncommitted.
</action_safety>
<structured_output_contract>
Report the root cause and fix, files touched, pytest and ruff outcomes with counts, and anything left
open or needing a decision.
</structured_output_contract>
```
Dispatch with [dispatch-and-poll.md](dispatch-and-poll.md), then review and land with
[review-and-land.md](review-and-land.md).
scripts/relay.mjs›
#!/usr/bin/env node
/**
* delegate-skills · claude-delegate · relay.mjs
*
* Dispatch a self-contained brief to a separate Claude Code CLI process
* (`claude -p`), capture the structured stream, and write a stable result the
* orchestrator can review.
*
* Trust posture: this relay makes no network calls of its own, reads or writes
* no credentials, sends no telemetry, and has no dependencies (Node built-ins
* only). It launches `claude` and uses `git status --porcelain`; on Windows it
* may also use cmd.exe for an npm .cmd shim and taskkill to terminate a process
* tree. Claude authenticates and makes its own network calls exactly as it does
* at the terminal.
*
* The relay never commits. Its write-capable profiles use string rules to deny
* common direct forms of `git commit`, `git push`, and nested `claude`, plus
* any command containing `claude-delegate`. Aliases, scripts, and wrappers can
* bypass those speed bumps; the brief's no-commit instruction and orchestrator
* review remain the boundary. The orchestrator re-runs the project gates and
* lands the work.
*
* Normal runs use Claude's `acceptEdits` permission mode with an explicit tool
* surface (Read, Glob, Grep, Edit, Write, and the platform shell). On macOS,
* Linux, and WSL2, an inline settings file requires Claude's shell sandbox,
* disables its unsandboxed escape hatch, and auto-approves commands that stay
* sandboxed so non-interactive gates can run. That sandbox covers shell
* processes and their children only; merged local or managed settings can
* affect its effective boundary, and it is not a universal boundary around
* Claude Code. Native Windows has no Claude shell sandbox, so the relay
* pre-approves PowerShell there and documents that it is not OS-isolated.
*
* `--read-only` uses plan mode and exposes only Read, Glob, and Grep: no edit,
* write, shell, MCP, skill, command, or Agent tool. The relay also compares git
* porcelain before and after, and also fingerprints working-tree and index state for paths
* that were already dirty, and emits `readOnlyViolation`. This is a tripwire,
* not an OS boundary: it reports detected Git-visible changes, it does not prevent them. It is
* three-valued - `null` means coverage was incomplete (git unavailable, a
* submodule, an unreadable path), never "nothing happened". Git-ignored paths
* are outside it.
*
* Every profile disables configured MCP discovery and Claude.ai connectors,
* denies all MCP tools, and disables skills and commands for the child while
* preserving normal authentication, CLAUDE.md discovery, hooks, and the rest of
* the inherited environment. Hooks are outside the tool restriction and can
* mutate a read-only run; readOnlyViolation is the tripwire. When nested under
* Claude Code, the child environment removes only CLAUDECODE;
* CLAUDE_CODE_CHILD_SESSION and credential variables are preserved. The same
* environment is used for version preflight.
*
* Usage:
* node relay.mjs --brief <file> [options]
* cat brief.txt | node relay.mjs [options]
*
* Options:
* --brief <file> Brief path. Omit to read the brief from stdin.
* --cd <dir> Child cwd (default: current directory).
* --lane <name> Fleet lane from delegate-setup config (dials apply; explicit flags win).
* --out-dir <dir> Artifact directory (default: system temp).
* --timeout <dur> Relay watchdog; off by default. Use h/m/s,
* such as 30m, 90s, or 1h30m.
* --model <name> Claude model alias or full name.
* --effort <level> low | medium | high | xhigh | max | ultracode
* --max-turns <n> Positive agentic-turn limit.
* --max-budget-usd <amount> Positive decimal spend limit.
* --resume-last Map to Claude's --continue.
* --session <id> Map to Claude's --resume <id>.
* Mutually exclusive with --resume-last.
* --read-only Plan mode with Read, Glob, and Grep only.
* --dangerously-skip-permissions Opt into Claude's bypassPermissions mode.
* Mutually exclusive with --read-only.
* -h, --help Show this help.
*
* Claude is always launched with:
* -p --output-format stream-json --verbose
* The brief is sent through stdin, never argv. `--bg` and `--bare` are not used.
*
* Artifacts:
* brief.txt exact brief sent on stdin
* events.jsonl raw Claude stdout stream
* final.txt final result event's text
* stderr.txt complete Claude stderr
* profile.json inspectable per-run settings passed with --settings
* result.json delegate-relay.result.v1
*
* Exit codes: usage errors exit 2 before result.json; a missing `claude` exits
* 127 with status "claude_unavailable"; otherwise the exit mirrors Claude, with
* a forced non-zero code for timeout or a terminal error result. Once the brief
* and artifact directory validate, result.json is written for completed,
* failed, timeout, aborted, and claude_unavailable outcomes.
*
* Native Windows launch behavior supports a directly spawned claude.exe and an
* npm claude.cmd shim through cmd.exe with strictly serialized arguments. It
* remains pending native-Windows verification.
*/
import { execFileSync, spawn, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
accessSync,
appendFileSync,
constants as fsConstants,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
renameSync,
statSync,
writeFileSync,
lstatSync,
readlinkSync,
openSync,
readSync,
closeSync,
realpathSync,
} from "node:fs";
import {basename, delimiter, join, relative, resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { constants as osConstants, tmpdir } from "node:os";
import { StringDecoder } from "node:string_decoder";
import { TextDecoder } from "node:util";
const MAX_BUFFERED_CHARS = 1_048_576;
const SCHEMA = "delegate-relay.result.v1";
const MAX_BRIEF_BYTES = 10_000_000;
const MAX_TIMER_MS = 2_147_483_647;
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max", "ultracode"]);
const SAFE_MODEL = /^[A-Za-z0-9][A-Za-z0-9._:@\/\[\]-]*$/;
const SAFE_SESSION = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
const IMPLEMENTER_KEY = "claude";
function makeEventScanner(onObject) {
let buf = "";
let index = 0;
let depth = 0;
let start = -1;
let inString = false;
let escaped = false;
return (chunk) => {
if (!chunk) return;
buf += chunk;
for (;;) {
while (index < buf.length) {
const ch = buf[index];
// Only track strings inside an object (depth > 0). At depth 0 we are
// skipping a junk prefix, and an unmatched `"` there must not swallow the
// real `{...}` that follows in the same chunk.
if (inString) {
if (escaped) escaped = false;
else if (ch === "\\") escaped = true;
else if (ch === '"') inString = false;
} else if (ch === '"') {
if (depth > 0) inString = true;
} else if (ch === "{") {
if (depth === 0) start = index;
depth += 1;
} else if (ch === "}") {
if (depth > 0) {
depth -= 1;
if (depth === 0 && start !== -1) {
const slice = buf.slice(start, index + 1);
try { onObject(JSON.parse(slice)); } catch { /* skip malformed */ }
start = -1;
}
}
}
index += 1;
}
if (depth === 0 || start === -1 || buf.length - start <= MAX_BUFFERED_CHARS) break;
// A complete object may exceed the retained-input cap within this chunk.
// Drop only an oversized partial, then rescan its suffix so a later
// concatenated event is not lost.
buf = buf.slice(start + MAX_BUFFERED_CHARS);
index = 0;
start = -1;
depth = 0;
inString = false;
escaped = false;
}
if (depth > 0 && start !== -1) {
if (start > 0) {
buf = buf.slice(start);
index -= start;
start = 0;
}
} else {
buf = "";
index = 0;
start = -1;
}
};
}
function applyFleetLane(opts, flagged) {
if (!opts.lane) return;
const script = join(dirname(fileURLToPath(import.meta.url)), "../../delegate-setup/scripts/lane.mjs");
if (!existsSync(script)) {
fail("--lane requires the delegate-setup skill installed beside this relay");
}
const r = spawnSync(
process.execPath,
[script, "resolve", "--cwd", opts.cd, "--lane", opts.lane, "--implementer", IMPLEMENTER_KEY],
{ encoding: "utf8", env: process.env },
);
if (r.error) fail(`lane resolve failed: ${r.error.message}`);
if (r.status !== 0) {
fail((r.stderr || "lane resolve failed").trim().replace(/^lane\.mjs:\s*/, ""));
}
let resolved;
try {
const lines = (r.stdout || "").trim().split("\n").filter(Boolean);
resolved = JSON.parse(lines[lines.length - 1]);
} catch {
fail("lane resolve returned invalid JSON");
}
opts.laneSource = resolved.source;
for (const [field, value] of Object.entries(resolved.dials || {})) {
if (flagged.has(field)) continue;
if (field === "autonomy" && (flagged.has("autonomy") || flagged.has("sandbox") || flagged.has("readOnly"))) continue;
if (field === "agent" && (flagged.has("agent") || flagged.has("readOnly"))) continue;
if (field === "sandbox" && (flagged.has("sandbox") || flagged.has("readOnly"))) continue;
if (field === "permissionMode" && (flagged.has("permissionMode") || flagged.has("readOnly"))) continue;
if (field === "planOnly" && (flagged.has("planOnly") || flagged.has("readOnly"))) continue;
if (
field === "readOnly" &&
(flagged.has("readOnly") || flagged.has("dangerouslySkipPermissions"))
) {
continue;
}
if (field === "force" && flagged.has("force")) continue;
opts[field] = value;
}
}
function fail(message, code = 2) {
process.stderr.write(`relay: ${message}\n`);
process.exit(code);
}
function parseDuration(duration) {
const match = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/.exec(duration);
if (!match || (!match[1] && !match[2] && !match[3])) return null;
try {
const seconds =
BigInt(match[1] || 0) * 3600n +
BigInt(match[2] || 0) * 60n +
BigInt(match[3] || 0);
const milliseconds = seconds * 1000n;
if (milliseconds <= 0n || milliseconds > BigInt(MAX_TIMER_MS)) return null;
return Number(milliseconds);
} catch {
return null;
}
}
function parseArgs(argv) {
const flagged = new Set();
const opts = {
lane: null,
laneSource: null,
brief: null,
cd: process.cwd(),
outDir: null,
timeout: null,
model: null,
effort: null,
maxTurns: null,
maxBudgetUsd: null,
resumeLast: false,
session: null,
readOnly: false,
dangerouslySkipPermissions: false,
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
const next = () => {
const value = argv[i + 1];
if (value === undefined) fail(`${arg} requires a value`);
i += 1;
return value;
};
switch (arg) {
case "-h":
case "--help":
process.stdout.write(headerComment());
process.exit(0);
break;
case "--brief": opts.brief = next(); break;
case "--cd": opts.cd = resolve(next()); break;
case "--lane": opts.lane = next(); break;
case "--out-dir": opts.outDir = resolve(next()); break;
case "--timeout": opts.timeout = next(); flagged.add("timeout"); break;
case "--model": opts.model = next(); flagged.add("model"); break;
case "--effort": opts.effort = next(); flagged.add("effort"); break;
case "--max-turns": opts.maxTurns = next(); break;
case "--max-budget-usd": opts.maxBudgetUsd = next(); break;
case "--resume-last": opts.resumeLast = true; break;
case "--session": opts.session = next(); break;
case "--read-only": opts.readOnly = true; flagged.add("readOnly"); break;
case "--dangerously-skip-permissions":
opts.dangerouslySkipPermissions = true;
flagged.add("dangerouslySkipPermissions");
break;
default:
fail(`unknown option: ${arg}`);
}
}
applyFleetLane(opts, flagged);
if (opts.resumeLast && opts.session) {
fail("--resume-last and --session are mutually exclusive; pass only one");
}
if (opts.readOnly && opts.dangerouslySkipPermissions) {
fail("--read-only and --dangerously-skip-permissions are mutually exclusive");
}
if (opts.timeout !== null && parseDuration(opts.timeout) === null) {
fail(`--timeout "${opts.timeout}" is invalid or too long; use a positive h/m/s duration no longer than about 24 days`);
}
if (opts.model !== null && !SAFE_MODEL.test(opts.model)) {
fail("--model contains unsupported characters (allowed: letters, digits, . _ : @ / [ ] -)");
}
if (opts.session !== null && !SAFE_SESSION.test(opts.session)) {
fail("--session contains unsupported characters (allowed: letters, digits, . _ : -)");
}
if (opts.effort !== null && !EFFORT_LEVELS.has(opts.effort)) {
fail(`invalid --effort "${opts.effort}" (expected: ${[...EFFORT_LEVELS].join(", ")})`);
}
if (opts.maxTurns !== null) {
const turns = Number(opts.maxTurns);
if (!/^[1-9]\d*$/.test(opts.maxTurns) || !Number.isSafeInteger(turns)) {
fail("--max-turns must be a positive safe integer");
}
}
if (opts.maxBudgetUsd !== null) {
const budget = Number(opts.maxBudgetUsd);
if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(opts.maxBudgetUsd) || !Number.isFinite(budget) || budget <= 0) {
fail("--max-budget-usd must be a positive decimal number");
}
}
try {
if (!statSync(opts.cd).isDirectory()) fail(`--cd is not a directory: ${opts.cd}`);
} catch {
fail(`--cd directory not found: ${opts.cd}`);
}
if (opts.outDir && existsSync(opts.outDir)) {
try {
if (!statSync(opts.outDir).isDirectory()) fail(`--out-dir is not a directory: ${opts.outDir}`);
} catch {
fail(`cannot inspect --out-dir: ${opts.outDir}`);
}
}
return opts;
}
function headerComment() {
const src = readFileSync(new URL(import.meta.url), "utf8");
const match = src.match(/\/\*\*([\s\S]*?)\*\//);
if (!match) return "relay.mjs — dispatch a brief to a separate claude -p session\n";
return `${match[1].replace(/^\s*\* ?/gm, "").trim()}\n`;
}
function readBrief(opts) {
if (opts.brief) {
if (!existsSync(opts.brief)) fail(`brief file not found: ${opts.brief}`);
try {
return readFileSync(opts.brief, "utf8");
} catch (error) {
fail(`cannot read brief file: ${error && error.message ? error.message : String(error)}`);
}
}
if (process.stdin.isTTY) {
fail("no --brief given and stdin is a TTY; pass --brief <file> or pipe the brief on stdin");
}
try {
return readFileSync(0, "utf8");
} catch {
return "";
}
}
function childEnvironment() {
const env = { ...process.env };
if (process.platform === "win32") {
for (const key of Object.keys(env)) {
if (key.toUpperCase() === "CLAUDECODE") delete env[key];
}
} else {
delete env.CLAUDECODE;
}
return env;
}
function environmentValue(env, name) {
if (Object.prototype.hasOwnProperty.call(env, name)) return env[name];
if (process.platform !== "win32") return undefined;
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
return key ? env[key] : undefined;
}
function resolveClaudeLauncher(env, cwd) {
const pathValue = environmentValue(env, "PATH");
if (!pathValue) return null;
const pathEntries = pathValue.split(delimiter).map((entry) => entry.replace(/^"(.*)"$/, "$1"));
if (process.platform === "win32") {
const pathExt = (environmentValue(env, "PATHEXT") || ".COM;.EXE;.BAT;.CMD")
.split(";")
.map((value) => value.trim().toLowerCase())
.filter(Boolean);
for (const entry of pathEntries) {
const directory = resolve(cwd, entry || ".");
for (const extension of pathExt) {
const candidate = join(directory, `claude${extension}`);
try {
if (!statSync(candidate).isFile()) continue;
const kind = extension === ".cmd" || extension === ".bat" ? "cmd" : "direct";
return { path: candidate, kind };
} catch {
// Keep searching PATH.
}
}
}
return null;
}
for (const entry of pathEntries) {
const candidate = join(resolve(cwd, entry || "."), "claude");
try {
accessSync(candidate, fsConstants.X_OK);
if (statSync(candidate).isFile()) return { path: candidate, kind: "direct" };
} catch {
// Keep searching PATH.
}
}
return null;
}
function quoteCmdArgument(value) {
// cmd expands %VAR% even inside quotes, and embedded quotes/newlines can break
// the /c command boundary. Reject those rare path characters instead of
// pretending an npm .cmd launch is safe. All user-selectable token values are
// independently restricted by parseArgs.
if (/[\0\r\n"%!]/.test(value)) {
throw new Error("the npm claude.cmd launch cannot safely serialize an argument containing %, !, a quote, or a newline; use the native claude.exe or a safe artifact path");
}
return `"${value}"`;
}
function launchSpec(launcher, argv, env) {
if (launcher.kind === "direct") {
return { command: launcher.path, argv, windowsVerbatimArguments: false };
}
const commandLine = [launcher.path, ...argv].map(quoteCmdArgument).join(" ");
const commandProcessor = environmentValue(env, "COMSPEC") || "cmd.exe";
return {
command: commandProcessor,
argv: ["/d", "/v:off", "/s", "/c", `"${commandLine}"`],
// The final argv element is an intentionally serialized cmd /c program.
// Letting libuv quote it again would change cmd's nested-quote boundary.
windowsVerbatimArguments: true,
};
}
function preflightVersion(launcher, env, cwd) {
try {
const spec = launchSpec(launcher, ["--version"], env);
const probe = spawnSync(spec.command, spec.argv, {
cwd,
env,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 15_000,
windowsHide: true,
windowsVerbatimArguments: spec.windowsVerbatimArguments,
});
if (probe.error && probe.error.code === "ENOENT") return null;
if (probe.status !== 0) return "unknown";
return String(probe.stdout || "").trim() || "unknown";
} catch {
return "unknown";
}
}
function shellTool() {
return process.platform === "win32" ? "PowerShell" : "Bash";
}
function selectedPermissionMode(opts) {
if (opts.readOnly) return "plan";
if (opts.dangerouslySkipPermissions) return "bypassPermissions";
return "acceptEdits";
}
function toolSurface(opts) {
if (opts.readOnly) return "Read,Glob,Grep";
return `Read,Glob,Grep,Edit,Write,${shellTool()}`;
}
function profileSettings(opts) {
const shell = shellTool();
const shellSandboxEnabled = !opts.readOnly && process.platform !== "win32";
return {
disableClaudeAiConnectors: true,
...(process.platform === "win32"
? { env: { CLAUDE_CODE_USE_POWERSHELL_TOOL: "1" } }
: {}),
permissions: {
deny: opts.readOnly
? []
: [
// Two-wildcard git forms require text after the subcommand, so the
// single-wildcard forms also catch `git -C <dir> push`; they also
// deny reads like `git log --grep push`. Anchor `claude` at command
// position so reads mentioning it remain allowed. Keep
// `claude-delegate` as a substring rule to stop recursive relay
// invocation; any command containing it is denied.
`${shell}(git commit *)`,
`${shell}(git * commit *)`,
`${shell}(git * commit)`,
`${shell}(git push *)`,
`${shell}(git * push *)`,
`${shell}(git * push)`,
`${shell}(claude *)`,
`${shell}(*claude-delegate*)`,
],
},
...(shellSandboxEnabled
? {
sandbox: {
enabled: true,
failIfUnavailable: true,
autoAllowBashIfSandboxed: true,
allowUnsandboxedCommands: false,
excludedCommands: [],
filesystem: {
disabled: false,
},
},
}
: {}),
};
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function prepareRun(opts, brief) {
const outDir =
opts.outDir ||
join(tmpdir(), "delegate-relay", `${basename(opts.cd) || "repo"}-${timestamp()}-${process.pid}`);
mkdirSync(outDir, { recursive: true });
const run = {
startedAt: new Date().toISOString(),
outDir,
briefPath: join(outDir, "brief.txt"),
eventsPath: join(outDir, "events.jsonl"),
finalPath: join(outDir, "final.txt"),
stderrPath: join(outDir, "stderr.txt"),
settingsPath: join(outDir, "profile.json"),
resultPath: join(outDir, "result.json"),
};
writeFileSync(run.briefPath, brief, "utf8");
writeFileSync(run.eventsPath, "", "utf8");
writeFileSync(run.finalPath, "", "utf8");
writeFileSync(run.stderrPath, "", "utf8");
writeFileSync(run.settingsPath, `${JSON.stringify(profileSettings(opts), null, 2)}\n`, "utf8");
return run;
}
function buildArgv(opts, run) {
const argv = [
"-p",
"--output-format", "stream-json",
"--verbose",
"--tools", toolSurface(opts),
"--strict-mcp-config",
"--disallowedTools", "mcp__*",
"--disable-slash-commands",
"--settings", run.settingsPath,
];
if (opts.readOnly) {
argv.push("--permission-mode", "plan");
} else if (opts.dangerouslySkipPermissions) {
argv.push("--dangerously-skip-permissions");
} else {
argv.push("--permission-mode", "acceptEdits");
// Supported platforms auto-approve only commands that remain inside
// Claude's sandbox. Native Windows has no shell sandbox, so approving its
// selected shell is the explicit tradeoff that keeps print mode headless.
if (process.platform === "win32") argv.push("--allowedTools", "PowerShell");
}
// The selected permission profile is passed on every invocation, including
// --continue/--resume, rather than assuming a session remembers it.
if (opts.resumeLast) argv.push("--continue");
else if (opts.session) argv.push("--resume", opts.session);
if (opts.model) argv.push("--model", opts.model);
if (opts.effort) argv.push("--effort", opts.effort);
if (opts.maxTurns) argv.push("--max-turns", opts.maxTurns);
if (opts.maxBudgetUsd) argv.push("--max-budget-usd", opts.maxBudgetUsd);
return argv;
}
function eventSessionId(event) {
return (
event.session_id ??
event.sessionId ??
(event.session && (event.session.id ?? event.session.session_id)) ??
null
);
}
function handleEvent(event, state, run) {
if (!event || typeof event !== "object") return;
const sessionId = eventSessionId(event);
if (typeof sessionId === "string" && sessionId) state.sessionId = sessionId;
if (event.type === "system" && event.subtype === "init") {
const mode = event.permissionMode ?? event.permission_mode;
if (typeof mode === "string" && mode) state.permissionMode = mode;
}
if (event.type !== "result") return;
state.sawResult = true;
state.resultSubtype = typeof event.subtype === "string" ? event.subtype : null;
state.resultIsError =
event.is_error === true ||
(state.resultSubtype !== null && /^error(?:_|$)/i.test(state.resultSubtype));
state.finalMessage = typeof event.result === "string" ? event.result : "";
state.usage = event.usage && typeof event.usage === "object" ? event.usage : null;
const cost = event.total_cost_usd ?? event.cost_usd;
state.totalCostUsd = typeof cost === "number" && Number.isFinite(cost) ? cost : null;
const turns = event.num_turns ?? event.numTurns;
state.numTurns = typeof turns === "number" && Number.isFinite(turns) ? turns : null;
writeFileSync(run.finalPath, state.finalMessage, "utf8");
}
// Porcelain status alone cannot see every write. A path that is " M file" before a run and
// " M file" after it produces an identical line, so comparing status lines proves nothing about
// its contents — which is why the read-only tripwire below fingerprints the already-dirty paths
// as well. Two sentinels stand for "could not fingerprint"; they are never treated as unchanged.
const FINGERPRINT_UNREADABLE = "<unreadable>";
const FINGERPRINT_DIRECTORY = "<directory>";
function gitRepoRoot(cwd) {
// Porcelain paths are relative to the repository ROOT, not to the directory git ran in
// (--porcelain forces status.relativePaths off). Joining them against a --cd that is a
// subdirectory would look for <repo>/src/src/file and find nothing at either end.
try {
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
cwd,
encoding: "utf8",
timeout: 10_000,
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "ignore"],
}).replace(/\n$/, "") || null;
} catch {
return null;
}
}
function gitStatusEntries(cwd) {
// -z so a path containing a space, a quote, or a newline stays one field rather than being
// quoted and escaped; -uall so an untracked directory is expanded into its files, because a
// collapsed "?? dir/" line never changes when a file inside it does.
try {
const output = execFileSync("git", ["status", "--porcelain", "-z", "-uall"], {
cwd,
timeout: 10_000,
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "ignore"],
maxBuffer: 64 * 1024 * 1024,
});
const fields = new TextDecoder("utf-8", { fatal: true }).decode(output)
.split("\0").filter((field) => field.length > 0);
const entries = [];
for (let i = 0; i < fields.length; i += 1) {
const entry = fields[i];
const status = entry.slice(0, 2);
const path = entry.slice(3);
// R and C can sit in EITHER status column, and under -z such an entry is followed by its
// origin path as its own unprefixed field. Consume that field in both cases. A rename
// origin belongs in the dirty set (the file moved away from it); a copy origin does not,
// since a copy source can be a perfectly clean file.
const renamed = status.includes("R");
const copied = status.includes("C");
let origin = null;
if (renamed || copied) {
i += 1;
origin = fields[i] ?? null;
}
entries.push({ status, path, origin });
}
return entries;
} catch {
return null;
}
}
function dirtyPaths(cwd) {
const entries = gitStatusEntries(cwd);
if (entries === null) return null;
const paths = [];
for (const entry of entries) {
paths.push(entry.path);
if (entry.status.includes("R") && entry.origin !== null) paths.push(entry.origin);
}
return paths;
}
function asciiFold(value) {
return value.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
}
function canonicalFilePath(path) {
const absolute = resolve(path);
let parent;
try { parent = realpathSync.native(dirname(absolute)); } catch { return absolute; }
const leaf = basename(absolute);
const canonical = join(parent, leaf);
try { lstatSync(canonical); } catch { return canonical; }
try {
const entries = readdirSync(parent);
if (entries.includes(leaf)) return canonical;
const matches = entries.filter((entry) => asciiFold(entry) === asciiFold(leaf));
return join(parent, matches.length === 1 ? matches[0] : leaf);
} catch {
return canonical;
}
}
function gitPathKey(root, path) {
let canonicalRoot;
try { canonicalRoot = realpathSync.native(root); } catch { canonicalRoot = resolve(root); }
const key = relative(canonicalRoot, canonicalFilePath(path));
return process.platform === "win32" ? key.replaceAll("\\", "/") : key;
}
function gitPathIsExcluded(root, path, excluded, foldedExcluded) {
return excluded.has(path) ||
(foldedExcluded.has(asciiFold(path)) && excluded.has(gitPathKey(root, join(root, path))));
}
function gitTripwireState(cwd, excludedPaths) {
const root = gitRepoRoot(cwd);
if (root === null) return null;
const entries = gitStatusEntries(cwd);
if (entries === null) return null;
const excluded = new Set(excludedPaths.map((path) => gitPathKey(root, path)));
const foldedExcluded = new Set([...excluded].map(asciiFold));
return entries.flatMap((entry) => [
[entry.status, "path", entry.path],
...(entry.origin === null ? [] : [[entry.status.replace(/[^RC]/g, " "), "origin", entry.origin]]),
]
.filter(([, , path]) => !gitPathIsExcluded(root, path, excluded, foldedExcluded)));
}
function pathFingerprint(absolutePath) {
// Identity, not just bytes: a retargeted symlink, a flipped mode bit, or a file replaced by a
// directory are all writes, and none of them change file contents.
let stats;
try {
stats = lstatSync(absolutePath);
} catch (error) {
// Absence is a state, not a failure - it differs from every real fingerprint, so a deletion
// or a re-creation still registers. Any other errno means we genuinely cannot tell.
return error && error.code === "ENOENT" ? "absent" : FINGERPRINT_UNREADABLE;
}
if (stats.isSymbolicLink()) {
try {
return `symlink:${readlinkSync(absolutePath, { encoding: "buffer" }).toString("hex")}`;
} catch {
return FINGERPRINT_UNREADABLE;
}
}
// A directory in the dirty set is a submodule, whose contents belong to another repository.
// Reported as unknown coverage rather than silently passed off as unchanged.
if (stats.isDirectory()) return FINGERPRINT_DIRECTORY;
if (!stats.isFile()) return FINGERPRINT_UNREADABLE;
let fd;
try {
// Streamed rather than read whole: an unignored multi-gigabyte artifact must not be pulled
// into memory just to answer whether it changed.
const hash = createHash("sha256");
fd = openSync(absolutePath, "r");
const buffer = Buffer.allocUnsafe(64 * 1024);
for (;;) {
const read = readSync(fd, buffer, 0, buffer.length, null);
if (read <= 0) break;
hash.update(buffer.subarray(0, read));
}
return `file:${(stats.mode & 0o7777).toString(8)}:${hash.digest("hex")}`;
} catch {
return FINGERPRINT_UNREADABLE;
} finally {
if (fd !== undefined) {
try { closeSync(fd); } catch { /* already closed */ }
}
}
}
function gitIndexFingerprints(root, paths) {
if (paths.length === 0) return new Map();
try {
const output = execFileSync("git", ["ls-files", "--stage", "-z"], {
cwd: root,
timeout: 10_000,
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "ignore"],
maxBuffer: 64 * 1024 * 1024,
});
const wanted = new Set(paths);
const prints = new Map(paths.map((path) => [path, []]));
for (const field of new TextDecoder("utf-8", { fatal: true }).decode(output).split("\0")) {
if (!field) continue;
const separator = field.indexOf("\t");
if (separator === -1) return null;
const path = field.slice(separator + 1);
if (wanted.has(path)) prints.get(path).push(field.slice(0, separator));
}
return prints;
} catch {
return null;
}
}
function fingerprintPaths(root, paths) {
// `complete` goes false the moment one path cannot be fingerprinted, so the caller reports
// "unknown" instead of an unearned clean bill of health.
const indexPrints = gitIndexFingerprints(root, paths);
const prints = new Map();
let complete = indexPrints !== null;
for (const path of paths) {
const file = pathFingerprint(join(root, path));
if (file === FINGERPRINT_UNREADABLE || file === FINGERPRINT_DIRECTORY) complete = false;
prints.set(path, { file, index: indexPrints?.get(path) ?? null });
}
return { prints, complete };
}
function fingerprintDirtyPaths(cwd, excludedPaths) {
// Only the already-dirty set is covered. A path that is clean at dispatch and gets written
// surfaces as a brand-new porcelain line anyway, and fingerprinting a whole repository per run
// would cost far more than the case it covers.
const root = gitRepoRoot(cwd);
if (root === null) return null;
const paths = dirtyPaths(cwd);
if (paths === null) return null;
const excluded = new Set(excludedPaths.map((path) => gitPathKey(root, path)));
const foldedExcluded = new Set([...excluded].map(asciiFold));
return {
root,
...fingerprintPaths(root, paths.filter((path) => !gitPathIsExcluded(root, path, excluded, foldedExcluded))),
};
}
function changedDirtyPaths(before) {
// Re-fingerprint exactly the baseline paths, not whatever happens to be dirty now: a path the
// run newly dirtied is already reported by the porcelain comparison, and letting an unreadable
// one of those blind this signal would be a regression, not caution.
if (!before) return { changed: [], complete: false };
const now = fingerprintPaths(before.root, [...before.prints.keys()]);
const changed = [];
for (const [path, print] of before.prints) {
const current = now.prints.get(path);
const fileKnown = print.file !== FINGERPRINT_UNREADABLE && current.file !== FINGERPRINT_UNREADABLE;
const fileChanged = fileKnown &&
!(print.file === FINGERPRINT_DIRECTORY && current.file === FINGERPRINT_DIRECTORY) &&
current.file !== print.file;
const indexChanged = print.index !== null && current.index !== null &&
JSON.stringify(current.index) !== JSON.stringify(print.index);
if (fileChanged || indexChanged) changed.push(path);
}
return { changed: changed.sort(), complete: before.complete && now.complete };
}
function readOnlyVerdict(beforeTree, afterTree, beforeFingerprints) {
// Three-valued on purpose. Proof of a write settles it even when the other signal is unknown;
// only when nothing is proven AND coverage is incomplete is the answer genuinely unknown.
// Collapsing that last case to false is the false assurance a tripwire must never give.
const changed = changedDirtyPaths(beforeFingerprints);
const porcelainMoved =
beforeTree !== null && afterTree !== null && JSON.stringify(beforeTree) !== JSON.stringify(afterTree);
if (porcelainMoved || changed.changed.length > 0) return true;
if (beforeTree === null || afterTree === null || !changed.complete) return null;
return false;
}
function gitTouchedFiles(cwd) {
try {
const output = execFileSync("git", ["status", "--porcelain"], {
cwd,
encoding: "utf8",
timeout: 10_000,
killSignal: "SIGKILL",
stdio: ["ignore", "pipe", "ignore"],
maxBuffer: 64 * 1024 * 1024,
});
return output.split("\n").map((line) => line.trimEnd()).filter(Boolean);
} catch {
return null;
}
}
function stderrTail(stderrPath) {
try {
return readFileSync(stderrPath, "utf8")
.split(/\r?\n/)
.map((line) => line.trimEnd())
.filter((line) => line.trim())
.slice(-20);
} catch {
return [];
}
}
function writeJsonAtomic(path, value) {
const temporary = `${path}.tmp-${process.pid}`;
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, "utf8");
renameSync(temporary, path);
}
function makeResultWriter(opts, version, run, state, beforeTree, beforeFingerprints) {
return (extra) => {
const result = {
schema: SCHEMA,
lane: opts.lane,
laneSource: opts.laneSource,
tool: "claude",
workdir: opts.cd,
model: opts.model,
effort: opts.effort,
maxTurns: opts.maxTurns === null ? null : Number(opts.maxTurns),
maxBudgetUsd: opts.maxBudgetUsd === null ? null : Number(opts.maxBudgetUsd),
timeout: opts.timeout,
readOnly: opts.readOnly,
resumed: Boolean(opts.resumeLast || opts.session),
resumeLast: opts.resumeLast,
permissionMode: state.permissionMode || selectedPermissionMode(opts),
toolSurface: toolSurface(opts).split(","),
shellSandbox: opts.readOnly
? "not-applicable"
: process.platform === "win32"
? "unsupported-on-native-windows"
: "strict",
dangerouslySkipPermissions: opts.dangerouslySkipPermissions,
claudeVersion: version,
sessionId: state.sessionId,
resultSubtype: state.resultSubtype,
numTurns: state.numTurns,
usage: state.usage,
totalCostUsd: state.totalCostUsd,
startedAt: run.startedAt,
finishedAt: new Date().toISOString(),
briefPath: run.briefPath,
eventsPath: run.eventsPath,
finalPath: run.finalPath,
stderrPath: run.stderrPath,
settingsPath: run.settingsPath,
...extra,
};
if (opts.readOnly) {
result.readOnlyViolation = readOnlyVerdict(
beforeTree,
gitTripwireState(opts.cd, [run.briefPath, run.eventsPath, run.finalPath, run.stderrPath, run.settingsPath, run.resultPath]),
beforeFingerprints,
);
}
writeJsonAtomic(run.resultPath, result);
return result;
};
}
function killChild(child, signal = "SIGTERM") {
if (!child || !child.pid) return;
if (process.platform === "win32") {
if (signal !== "SIGTERM") return;
try {
execFileSync("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
stdio: ["ignore", "ignore", "inherit"],
});
} catch {
// The process tree already exited.
}
return;
}
try {
process.kill(-child.pid, signal);
} catch {
try {
child.kill(signal);
} catch {
// The process group already exited.
}
}
}
function reportUnavailable(writeResult, opts, run) {
const result = writeResult({
status: "claude_unavailable",
exitCode: 127,
signal: null,
finalMessage: "",
touchedFiles: gitTouchedFiles(opts.cd),
});
printSummary(result, run.resultPath);
process.stderr.write("relay: `claude` not found on PATH. Install Claude Code and run `claude auth login`.\n");
process.exit(127);
}
function dispatch(opts, brief, launcher, env, run, state, writeResult) {
let child;
try {
const spec = launchSpec(launcher, buildArgv(opts, run), env);
child = spawn(spec.command, spec.argv, {
cwd: opts.cd,
env,
stdio: ["pipe", "pipe", "pipe"],
detached: process.platform !== "win32",
windowsHide: true,
windowsVerbatimArguments: spec.windowsVerbatimArguments,
});
} catch (error) {
const result = writeResult({
status: "failed",
exitCode: 1,
signal: null,
finalMessage: state.finalMessage,
touchedFiles: gitTouchedFiles(opts.cd),
error: error && error.message ? error.message : String(error),
});
printSummary(result, run.resultPath);
process.exit(1);
return;
}
const stdoutDecoder = new StringDecoder("utf8");
const scan = makeEventScanner((event) => handleEvent(event, state, run));
child.stdout.on("data", (chunk) => {
appendFileSync(run.eventsPath, chunk);
scan(stdoutDecoder.write(chunk));
});
child.stderr.on("data", (chunk) => {
process.stderr.write(chunk);
appendFileSync(run.stderrPath, chunk);
});
let settled = false;
let watchdogFired = false;
let watchdogTimer = null;
let sigkillTimer = null;
const timeoutMs = opts.timeout === null ? null : parseDuration(opts.timeout);
if (timeoutMs !== null) {
watchdogTimer = setTimeout(() => {
watchdogFired = true;
child.once("exit", () => {
child.stdout.destroy();
child.stderr.destroy();
});
killChild(child);
sigkillTimer = setTimeout(() => {
if (!settled) killChild(child, "SIGKILL");
}, 10_000);
}, timeoutMs);
}
const clearWatchdog = () => {
if (watchdogTimer) clearTimeout(watchdogTimer);
if (sigkillTimer) clearTimeout(sigkillTimer);
};
for (const signalName of ["SIGTERM", "SIGINT", "SIGHUP"]) {
process.on(signalName, () => {
if (settled) return;
settled = true;
clearWatchdog();
child.once("exit", () => {
child.stdout.destroy();
child.stderr.destroy();
});
killChild(child);
const exitCode = 128 + (osConstants.signals[signalName] || 15);
const fields = () => ({
status: "aborted",
exitCode,
signal: signalName,
finalMessage: state.finalMessage,
touchedFiles: gitTouchedFiles(opts.cd),
stderrTail: stderrTail(run.stderrPath),
error: `the relay was killed by ${signalName}; claude was terminated with it — inspect the working tree before re-dispatching`,
});
const result = writeResult(fields());
printSummary(result, run.resultPath);
setTimeout(() => {
killChild(child, "SIGKILL");
// Claude may flush files and terminal metadata while handling SIGTERM.
// Rebuild every dynamic field, especially touchedFiles/readOnlyViolation.
writeResult(fields());
process.exit(exitCode);
}, 2000);
});
}
child.on("error", (error) => {
if (settled) return;
settled = true;
clearWatchdog();
const unavailable = error && error.code === "ENOENT";
const result = writeResult({
status: unavailable ? "claude_unavailable" : "failed",
exitCode: unavailable ? 127 : 1,
signal: null,
finalMessage: state.finalMessage,
touchedFiles: gitTouchedFiles(opts.cd),
...(unavailable ? {} : { stderrTail: stderrTail(run.stderrPath) }),
error: error && error.message ? error.message : String(error),
});
printSummary(result, run.resultPath);
process.exit(result.exitCode);
});
child.on("close", (code, signalName) => {
if (settled) return;
settled = true;
clearWatchdog();
scan(stdoutDecoder.end());
if (watchdogFired) killChild(child, "SIGKILL");
const missingResult = !state.sawResult;
const terminalError = state.resultIsError || missingResult;
const succeeded = code === 0 && !watchdogFired && !terminalError;
const mapped =
code ??
(signalName && osConstants.signals[signalName]
? 128 + osConstants.signals[signalName]
: 1);
const exitCode = succeeded ? 0 : mapped === 0 ? 1 : mapped;
const status = succeeded ? "completed" : watchdogFired ? "timeout" : "failed";
let error = null;
if (watchdogFired) {
error = `claude did not finish within --timeout ${opts.timeout}; killed by the relay watchdog`;
} else if (missingResult) {
error = "claude exited without a terminal result event; inspect events.jsonl and stderr.txt";
} else if (state.resultIsError) {
error = `claude returned an error result${state.resultSubtype ? ` (${state.resultSubtype})` : ""}`;
}
const result = writeResult({
status,
exitCode,
signal: signalName ?? null,
finalMessage: state.finalMessage,
touchedFiles: gitTouchedFiles(opts.cd),
...(succeeded ? {} : { stderrTail: stderrTail(run.stderrPath) }),
...(error ? { error } : {}),
});
printSummary(result, run.resultPath);
process.exit(result.exitCode);
});
// A launch failure can surface on child and on its stdin. The child error
// handler owns the outcome, so the pipe error is intentionally swallowed.
child.stdin.on("error", () => {});
child.stdin.end(brief);
}
function printSummary(result, resultPath) {
const lines = [];
lines.push("");
lines.push(
`relay: ${result.status} (exit ${result.exitCode}${result.signal ? `, killed by ${result.signal}` : ""}) · claude ${result.claudeVersion ?? "?"}`,
);
lines.push(`permission mode: ${result.permissionMode} · shell sandbox: ${result.shellSandbox}`);
if (result.dangerouslySkipPermissions) {
lines.push("warning: bypassPermissions was explicitly enabled; direct file tools can reach beyond normal permission boundaries.");
}
if (result.readOnlyViolation === true) {
lines.push("warning: a git-visible change was detected during this --read-only run; inspect the working tree immediately.");
} else if (result.readOnlyViolation === null) {
lines.push("warning: this --read-only tripwire had incomplete coverage; inspect the working tree directly.");
}
if (result.signal === "SIGKILL" && result.status === "failed") {
lines.push("hint: the host killed claude (commonly an OOM killer or supervisor timeout); inspect the tree and host resources.");
}
if (result.resumed) lines.push("mode: resumed an existing Claude session");
if (result.sessionId) lines.push(`session id (resume with: --session ${result.sessionId}): ${result.sessionId}`);
if (result.resultSubtype) lines.push(`result subtype: ${result.resultSubtype}`);
if (typeof result.numTurns === "number") lines.push(`turns: ${result.numTurns}`);
if (typeof result.totalCostUsd === "number") lines.push(`cost: $${result.totalCostUsd}`);
if (result.usage) {
const input = result.usage.input_tokens ?? result.usage.inputTokens ?? "?";
const output = result.usage.output_tokens ?? result.usage.outputTokens ?? "?";
lines.push(`tokens: in ${input}, out ${output}`);
}
const touched = result.touchedFiles;
if (touched === null) {
lines.push("touched files: git unavailable — inspect the working tree directly");
} else {
lines.push(`touched files: ${touched.length}`);
for (const file of touched.slice(0, 40)) lines.push(` ${file}`);
if (touched.length > 40) lines.push(` … and ${touched.length - 40} more`);
}
if (result.stderrTail && result.stderrTail.length) {
lines.push("last stderr:");
for (const line of result.stderrTail.slice(-8)) lines.push(` ${line}`);
}
lines.push("");
lines.push("--- claude final report ---");
lines.push(result.finalMessage || "(no final result text captured)");
lines.push("--- end report ---");
lines.push("");
lines.push(`result: ${resultPath}`);
lines.push("relay does not commit. Review the diff, re-run the project gates yourself, then land it from the orchestrator.");
process.stdout.write(`${lines.join("\n")}\n`);
}
function main() {
const opts = parseArgs(process.argv.slice(2));
const brief = readBrief(opts);
if (!brief.trim()) fail("empty brief (pass --brief <file> or pipe the brief on stdin)");
const briefBytes = Buffer.byteLength(brief, "utf8");
if (briefBytes > MAX_BRIEF_BYTES) {
fail(`brief is ${Math.ceil(briefBytes / 1_000_000)}MB; Claude Code caps piped stdin at 10MB. Put large context in the workspace and reference it from a smaller brief.`);
}
const env = childEnvironment();
const launcher = resolveClaudeLauncher(env, opts.cd);
let run;
try {
run = prepareRun(opts, brief);
} catch (error) {
fail(`cannot prepare run artifacts: ${error && error.message ? error.message : String(error)}`);
}
// Capture after relay artifacts exist so an --out-dir inside the worktree does
// not make the relay itself look like a read-only violation.
const relayArtifacts = [run.briefPath, run.eventsPath, run.finalPath, run.stderrPath, run.settingsPath, run.resultPath];
const beforeTree = opts.readOnly ? gitTripwireState(opts.cd, relayArtifacts) : null;
// Working-tree and index state for paths that are ALREADY dirty. Their porcelain lines will not
// move if the run edits them, so the line comparison above cannot see those writes on its own.
const beforeFingerprints = opts.readOnly ? fingerprintDirtyPaths(opts.cd, relayArtifacts) : null;
const version = launcher ? preflightVersion(launcher, env, opts.cd) : null;
const state = {
sessionId: opts.session,
permissionMode: selectedPermissionMode(opts),
sawResult: false,
resultSubtype: null,
resultIsError: false,
finalMessage: "",
numTurns: null,
usage: null,
totalCostUsd: null,
};
const writeResult = makeResultWriter(opts, version, run, state, beforeTree, beforeFingerprints);
if (!launcher || version === null) {
reportUnavailable(writeResult, opts, run);
return;
}
dispatch(opts, brief, launcher, env, run, state, writeResult);
}
main();
SKILL.md›
---
name: claude-delegate
description: >-
Delegate a coding task to a separate Claude Code CLI process or another Claude session as an
implementer, then review its diff and land it yourself. Use only when the user explicitly asks to
delegate implementation to Claude Code, another Claude session, or the `claude` CLI — for example,
"have another Claude implement this", "delegate this to Claude Code", or "run this queue through a
separate Claude session." Do not trigger merely because the current orchestrator is Claude, and do
not use when the user asks the current Claude to implement directly without delegation.
license: MIT
compatibility: Requires the `claude` CLI (Claude Code) installed and authenticated, Node 18+, and git. The orchestrating agent must be able to run shell commands and read files. Claude's shell sandbox requires macOS, Linux, or WSL2; native Windows launch is pending verification.
metadata:
version: 0.5.0
---
# Claude Delegate
You are the **orchestrator**. Delegate one bounded coding task to a separate **implementer** — a Claude
Code CLI session — then review what it produced and land it yourself. You write the brief and own the
judgment; the separate Claude session edits the working tree; you verify and commit.
This skill is not a signal for the current Claude to implement directly. Use it only after the human
explicitly asks for delegation to another Claude Code process or session.
## When not to use this
- The human asked the current agent to implement the task directly.
- The task is small enough to do inline and the human did not request delegation.
- The `claude` CLI is missing or unauthenticated (`claude auth status`).
- The task needs a stronger host boundary than Claude Code's tool permissions and shell-only sandbox
provide. Use an isolated container or VM for that requirement.
## Prerequisites
1. `claude --version` succeeds.
2. `claude auth status` reports an authenticated session. On macOS the live credentials sit in the
login Keychain; when the orchestrator's own sandbox blocks Keychain access (Codex's sandbox
does), `claude` falls back to a possibly stale credentials file and reports `loggedIn: false`
even though the login is valid. Re-run the check — and the dispatch itself — with that sandbox
escalated or outside it before concluding the CLI is unauthenticated.
3. The target repository is the directory passed with `--cd`.
4. On Linux/WSL2, Claude's sandbox dependencies are installed. The normal relay profile is
configured to fail when the sandbox is unavailable instead of silently running shell commands
unsandboxed. Existing merged settings can still affect the effective boundary.
## The loop
### 1. Write the brief
The separate session has no orchestrator chat history. It receives the brief on stdin and can inspect
the target working tree.
Claude Code automatically discovers the target project's `CLAUDE.md` and normal local Claude
configuration because the relay does not use `--bare`. It does **not** generically auto-load
`AGENTS.md`. Read `AGENTS.md` yourself and copy every load-bearing constraint and the real gate
commands into the brief. Tell the implementer not to commit. Keep one task per brief.
Template and details: [references/writing-the-brief.md](references/writing-the-brief.md).
### 2. Dispatch
```bash
node "<skill-dir>/scripts/relay.mjs" --brief brief.txt --cd /path/to/repo
# review/diagnosis only: add --read-only
# continue the latest session: add --resume-last
# continue the recorded session: add --session <id>
# choose limits: add --max-turns 40 --max-budget-usd 10
# hard relay deadline: add --timeout 2h
# inspect every option: node .../relay.mjs --help
```
`<skill-dir>` is this installed skill directory, the folder containing this `SKILL.md`.
The relay runs `claude -p --output-format stream-json --verbose`, sends the brief through stdin, and
writes artifacts under the system temp directory by default. It never uses `--bg` or `--bare`, and it
never commits. See [references/dispatch-and-poll.md](references/dispatch-and-poll.md).
### 3. Wait
The relay blocks until Claude exits. Use the orchestrator's background-command facility, or run it in
the foreground and wait. Completion means the process exited and `result.json` exists.
- A pre-run usage error exits 2 and writes no `result.json`.
- A missing `claude` exits 127 and writes `status: "claude_unavailable"`.
- Timeout and caught relay signals terminate the whole implementer process tree and preserve an
outcome artifact.
Read `finalMessage`, `touchedFiles`, `resultSubtype`, and the raw artifact paths from `result.json`.
### 4. Review
Treat the implementer's report and gate outcomes as claims:
- Review edits to existing tests before a green gate means anything.
- Re-run the project's actual gates yourself.
- Read the complete diff against the brief, starting with `touchedFiles`.
- Inspect untracked and staged content as well as the ordinary diff.
- Run relevant guard skills if installed.
Full checklist: [references/review-and-land.md](references/review-and-land.md).
### 5. Land
The **orchestrator commits** only after the gates pass and the diff holds. For rework, resume the same
Claude session with a delta brief:
```bash
echo "Keep the implementation, replace the mocked DB test with the migrated fixture, and remove the
unused import." | node "<skill-dir>/scripts/relay.mjs" --session <id> --cd /path/to/repo
```
Review a resumed run exactly like the first run.
## Permission profiles
The normal profile is deliberately explicit:
- `acceptEdits` permission mode.
- Built-in tools restricted to Read, Glob, Grep, Edit, Write, and the platform shell.
- On macOS, Linux, and WSL2, Claude's shell sandbox is enabled with startup failure on missing
dependencies and no unsandboxed retry. Commands that stay sandboxed are auto-approved so ordinary
gates can run headlessly. The sandbox governs shell processes and their children only; merged
local or managed sandbox settings can add effective paths or exclusions.
- Configured MCP discovery and Claude.ai connectors are disabled, all MCP tools are denied, and
skills, commands, and Claude's Agent tool are unavailable to the child. Project `CLAUDE.md`, hooks,
normal authentication, session persistence, and other local settings still load.
- String rules deny common direct shell forms of `git commit`, `git push`, and nested `claude`, plus
any command containing `claude-delegate`. Aliases, scripts, and wrappers can bypass them, so they
are only a speed bump; the brief's no-commit instruction and orchestrator review remain the boundary.
Native Windows does not support Claude's shell sandbox. The relay restricts the tool surface and
pre-approves PowerShell so the run remains non-interactive, but that shell is not OS-isolated. Native
`claude.exe` and npm `claude.cmd` launch paths are implemented; Windows verification is pending.
`--read-only` uses `plan` mode with only Read, Glob, and Grep. It removes edit, write, and shell paths,
then compares parsed git porcelain and fingerprints the working-tree identity and index entries of
Git-visible paths that were already dirty.
`readOnlyViolation` is `true` when either signal proves a change, `false` when coverage is complete and
detects none, and `null` when coverage is incomplete. This is a reporting tripwire, not an OS boundary:
ignored paths and perfect restores are outside it, local hooks can write, and concurrent changes cannot
be attributed to Claude.
`--dangerously-skip-permissions` is an explicit opt-in to Claude's `bypassPermissions` mode. The
restricted tool surface, direct commit/push deny rules, and supported-platform shell sandbox remain,
but direct file tools can cross normal permission boundaries. Use it only with the human's explicit
acceptance.
## Complementary to native Claude features
Claude subagents, agent teams, and background sessions are useful when the current Claude environment
is already the orchestrator and native coordination is the goal. This skill is complementary: it
provides a cross-orchestrator contract — self-contained brief → dispatch → artifacts → review → land
— and keeps the commit with the orchestrator.
## References
- [references/writing-the-brief.md](references/writing-the-brief.md) — context, `CLAUDE.md` versus
`AGENTS.md`, real gates, report contract, and delta briefs.
- [references/dispatch-and-poll.md](references/dispatch-and-poll.md) — flags, profiles, artifacts,
`result.json`, polling, and failure recovery.
- [references/review-and-land.md](references/review-and-land.md) — generated-code review, the commit
boundary, and session rework.
- [references/multi-task-queues.md](references/multi-task-queues.md) — sequential queues, progress
tracking, constraint carry-forward, and final coherence.