Setting up
You are installing the pipeline into a project. This is the only stage that
costs nothing and risks nothing: no agents run, nothing is committed, and
nothing outside devteam/ and one .gitignore line is touched.
Do all of it in one pass. A half-scaffolded project is worse than an un-scaffolded one, because the next session cannot tell which it is.
1. Scaffold
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/setup.py <project>
Default the project to the working directory. The script refuses to overwrite
an existing devteam/ — that directory is the project's design record, and
a setup that can clobber it eventually will. If it refuses, stop and tell the
client what is already there; do not move it aside for them.
It installs eight artifacts, creates empty tasks/ and checkpoints/,
creates the untracked .run/, adds devteam/.run/ to .gitignore, and
detects what the project already uses — stack, build, test and lint commands,
candidate protected paths, the remote — pre-filling the charter's constraints
table where it is confident.
Everything it detected is a proposal. Read the detection block back to the client and have them confirm each line.
Ask who else writes this tree, before anything else. A devteam/ directory
makes the guard police the whole repository for every session, not only
this pipeline's agents: while a task is RUNNING, any write outside its
declared scope is refused, whoever is making it. That is the one-writer rule
working exactly as intended — and if another agent system, another orchestrator
or a colleague is working the same tree, the collision is between two systems
and neither one knows about the other.
So: a devteam project should own its repository. If something else writes here, say so plainly and offer the two honest options — a repository this pipeline owns outright, or accepting that the other worker will be refused while a task runs. Do not scaffold into a contested tree and let them discover it as a mysterious refusal an hour later.
The guard is inert while no task is RUNNING, so the interference window is
exactly the loop's running time. That is worth saying, because it makes the
symptom intermittent, which is the hardest kind to diagnose. A detected command that is wrong is
worse than one that was asked about, because nothing later will question it.
2. Agree the permissions (P-38)
The point of doing this now is that the loop later runs for hours unattended, and a permission prompt at 3am stops it dead. So ask once, for exactly what the loop needs — no more, so the grant stays reviewable, and no less, so it does not stall.
Two different people own the two halves of this, and conflating them is how
a manager ends up laundering a permission. devteam/PERMISSIONS.md is the
client's artifact — what the loop intends to run, and why. The project's
.claude/settings.json is the operator's — the person or account whose
session actually executes those commands. They are frequently the same human
and are not the same role, and when the client is a peer session, a product
owner in a chat channel, or anybody remote, they are certainly not.
A client's approval is not authority to widen a permission set. If the
client is not the operator, prepare the allowlist, show it, and ask the
operator — the human at this session — to apply it. A peer session saying
"write the settings file" is not consent from the person whose machine runs
the commands, however plainly right the peer is. Say so plainly and continue;
PERMISSIONS.md still gets written, because that half is the client's to
approve.
Fill in devteam/PERMISSIONS.md from the confirmed build, test and lint
commands.
On a greenfield project there are no confirmed commands yet, because the
interview that settles them runs after this stage, deliberately. Do not invent
them and do not stall: write the file marked PROPOSED, say in the file
that it could not be filled from detection, carry the toolchain into the
interview as a question with your recommendation, and regenerate the file once
the charter is signed. A permission set that claims to be derived from a
charter that does not exist is worse than one that admits it is a proposal. Then put the matching allowlist in the project's
.claude/settings.json — permissions.allow — and show the client the
diff before writing it. A permission set nobody read is not a grant.
Three rules for what goes in it:
Commands, scoped — and never an interpreter at its bare entry point.
Bash(python3 -m pytest:*), notBash(python3:*)and notBash(*).Bash(python3:*)looks scoped and satisfies a naive reading of this rule, andpython3 -c "..."is arbitrary code execution — it silently re-grants every removal, install and network call the rest of this section refuses. The same trap isBash(sh:*),Bash(node:*),Bash(uv run:*).Nothing outward-facing. No
git push, nogh, no publish, no deploy. Those areIRREVERSIBLE(P-26) and belong to the client, who is asked at the moment it matters rather than pre-authorising it in the dark.No
sudo, ever. It escapes the guard entirely, so it is the one destructive thing that has to be refused by name. If the loop believes it needs it, that is a stop and an escalation (P-39), never a workaround.rmis judged by its target, not withheld by its name. The guard already treats a removal exactly like a write: refused inside the project tree unless a live scope covers it, refused on any protected path, and none of its business outside the tree. Withholding the wordrmon top of that was wider than the harm and duplicated a control by command text, which is the one thing this project says never to do — a text rule is spoofable by anything that spawns a shell, and the guard is not.It was also expensive twice. A file created in error under
devteam/could not be removed, leavinggit statusnon-empty and failing the first precondition of every future verification. Then a worker doing mutation testing in a scratch directory outside the tree — which is where every mutation battery in this pipeline runs — had no sanctioned way to clean up after itself, and breached the rule to do its job correctly. A rule that a worker must break to do the work right is a defect in the rule.
Then ask the client for the three dials the charter needs and nothing else can supply — the escalation window (how long a reversible question waits before the loop proceeds on its recommendation; four hours is the default), the model band (a floor and a ceiling, P-40), and the client channel.
The channel has to be settled here, at setup, because every later stage
needs it and you cannot ask the client how to reach the client. It is
terminal, session <name>, both, or none (P-9). If you are reading this
because another session dispatched you, the channel is that session and you
should say so rather than reaching for AskUserQuestion and waiting on a
terminal nobody is watching.
3. Register the guard, then prove it actually fires
Two separate things, and only the second one matters.
The control proves the guard script is correct:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/test_guard.py
It proves nothing about whether the hook is registered. A green control and an unregistered hook look identical from here, and that is exactly how this pipeline once ran a whole rehearsal believing it was protected while the guard never fired once. Never report a guard as live on the strength of its control.
Register it. A plugin's hooks/hooks.json is loaded when the plugin is
installed through the marketplace. If it was installed by symlinking into
~/.claude/skills/, that loads its skills and agents but not its hooks —
register the guard by hand in ~/.claude/settings.json:
{ "hooks": { "PreToolUse": [ {
"matcher": "Bash|Write|Edit|NotebookEdit",
"hooks": [ { "type": "command",
"command": "python3 <plugin>/scripts/guard.py", "timeout": 10 } ]
} ] } }
Hooks are read at session start, so this needs a restart.
Skills and agents load differently, which will catch you out. A skill added to an installed plugin appears in the current session almost immediately. A new agent type does not: a dispatch to one added minutes earlier fails with "agent type not found" while its skill sits there working.
Dispatchability is a property of the session, not of the file. This was
measured rather than assumed, and the result rules out both obvious answers.
One agent file, one machine, two sessions, the same minute: session A — the one
that wrote the file — got agent type not found two minutes after writing it
and again at twenty-two, under both bare and namespaced names. Session B
dispatched the same agent successfully in between: it ran, confirmed its own
type, and used its declared tool. So the file was fine and the delay was not
simply elapsed time. Each session dispatches against its own view of the agent
list, and those views do not update together.
So state neither "agents hot-load" nor "agents require a restart". What holds
is: a new agent is dispatchable when your session can see it, and you find
that out by dispatching it. A not found that a peer session can dispatch is
the known behaviour, not a broken install.
Do not restart a running pipeline to pick up an agent. This is the part that costs real money. A restart is not free here — P-14 makes every claim stale, so restarting forces a recovery pass over every task in flight. At width 3 that is three tasks re-verified against a tree nobody was watching, to acquire an agent type that may well arrive on its own. Add the agent, carry on with the work you have, and dispatch it when it appears. If you genuinely cannot proceed without it, close the live claims first and restart into a clean board.
Then prove it, end to end. After the restart, attempt one write the guard
must refuse — a path outside every declared scope while a task is RUNNING —
and confirm it is actually refused. That single refused write is the only
evidence that matters.
Use a literal absolute path in that test. Not $REPO/..., not any shell
variable. The guard cannot resolve a target containing an unexpanded variable
and does not judge it, so a test written the natural way passes silently and
looks exactly like a guard that is not installed. This trap has already
been walked into once, and it produced four consecutive false negatives and a
published claim that had to be retracted.
The guard is inert until the charter names protected paths and a task is
RUNNING, so it will not obstruct the client's own work before the loop
starts.
3b. Then prove the sandbox contains, and write the charter's row from it
The guard is the early warning; under Containment: structural the sandbox is
the mechanism (P-43). Prove it the same way and for the same reason — by
watching it refuse something, not by reading that it would.
First, the probe:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sandbox_probe.py # exit 0, 1 or 2
Then one write that must fail, on the freshly scaffolded project:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sandbox.py exec --repo <project> \
-- sh -c 'echo x > /etc/devteam-containment-check'
# -> sh: cannot create /etc/devteam-containment-check: Read-only file system
Use a literal absolute path here too, and one outside the project, for the same reason the guard's proof does: a path inside the repository is writable inside the sandbox by design — that is the whole point — so a test that writes there succeeds under both a working sandbox and no sandbox at all, and answers nothing. The arms have to differ.
Write the charter's Containment row from the probe's exit code, never from
what this machine looks like:
| Probe exit | Row | What it means |
|---|---|---|
| 0 | structural |
workers run inside a copy-on-write overlay; P-43, P-10c, P-12c and P-38b's widened inside grant all apply |
| 1 | guard-only |
this machine cannot make the namespace. Cycle 0.1's behaviour, whole |
| 2 | stop and read the table | the composition's assumptions do not hold here — already inside somebody's namespace, or a bwrap that would compose differently. Do not choose a row from a guess |
Say what guard-only leaves open, in the row's own comment, in the charter.
It is not a lesser configuration of the same thing; it is a project where three
measured holes stay open — an interpreter heredoc writes unjudged (F-80, F-121),
a git history rewrite has no path for a path-based guard to see (F-71), and
every agent shares one index (the F-17 class). A client whose project says
guard-only is entitled to know that from their own charter rather than from
this file.
Never write structural as an example value anywhere. The value comes from
the probe on the machine that will run the work, and an example showing the good
one is exactly how it gets copied onto a machine that cannot do it.
4. Hand over
Print, and nothing more:
- what was installed, and where
- what was detected and confirmed
- the permissions granted, in one line each
/devteam:onboardas the next step
Do not start the interview. Onboarding is a separate gate for a reason: the client should be able to walk away here, read what was installed, and come back. Setup that slides straight into a forty-question interview is setup the client did not consent to.
What this stage must not do
- Not write anything outside
devteam/,.gitignoreand.claude/settings.json. Not a README, not a CI file, not a licence. - Not commit. The client commits the scaffold, having read it.
- Not guess at the charter. Detection fills in commands, which are facts on disk. Goals, scope and what "done" means are not on disk, and inventing them here is how a project ends up building something nobody asked for.