Skip to content

What happens when these plugins share a session ​

Nothing in a plugin's own README says what happens when another plugin is installed alongside it. This page collects what the guard hooks, the diff baselines, and the ambient plugins actually do when they overlap, grounded in hook source rather than in what a README implies. delegation.md covers the dispatch side; this page covers what happens once those dispatches — or a direct edit — land while a guard is watching.

Nine hooks already arbitrate every edit ​

Nine werkstoff plugins register a PreToolUse hook, and all nine are inert until the repository shows a specific piece of state — none of them polices an unrelated project the moment it happens to be installed. The fact a reader actually scans for first — will this fire on my repo, right now — is called out as its own line on every card, rather than buried in the third column of a five-column table.

andonWrite|Edit

hooks/andon_enforce.py

Inert unlessa ledger exists in the current working directory

Escape hatchenforcement: off in .claude/andon.local.md

zeugnisEdit|Write

scripts/hooks/guard_edit_scope.py

Inert unlessa remediation-scope lock is open at analysis/zeugnis/remediation_scope.json (found from the cwd or any ancestor); a mutation_harden lock allows test-path writes only

Escape hatchdelete the lock file, or run without --fix

zeugnisBash

scripts/hooks/guard_bash_scope.py

Inert unlessanalysis/zeugnis/ exists in the cwd or an ancestor; then mutmut run/apply, cosmic-ray exec and the in-place modes of cargo-mutants and Stryker are denied everywhere but a harness copy

Escape hatchrun the command outside a zeugnis-managed session, or remove analysis/zeugnis/

cupertinoSkill|Task|Agent|Write|Edit|Bash

hooks/pretooluse_guard.py

Inert unlessa .cupertino/ state directory exists

Escape hatchCUPERTINO_DISABLE_GUARD=1

befundWrite|Edit|MultiEdit

hooks/guard_target_edit.py

Inert unlessa befund edit-scope lock is open at analysis/befund/edit_scope.json

Escape hatchnamed in the hook's own deny message

taktSkill|Task|Agent|Write|Edit|MultiEdit

hooks/takt_guard.py

Inert unlessa beat declaration exists at .claude/takt.local.md

Escape hatchTAKT_DISABLE_GUARD=1

lehreWrite|Edit|MultiEdit

hooks/lehre_guard.py

Inert unlessa doctrine is declared at .lehre/ruleset.json

Escape hatchLEHRE_DISABLE_GUARD=1

nacharbeitWrite|Edit|MultiEdit|Bash

hooks/nacharbeit_guard.py

Inert unlessa fix pass has opened the lock at analysis/nacharbeit/fix_scope.json

Escape hatchNACHARBEIT_DISABLE_GUARD=1, or post_fix_check.py --release-lock

arbeitsplanSkill|Task|Agent|Write|Edit|MultiEdit

hooks/arbeitsplan_guard.py

Inert unlessanalysis/arbeitsplan/run_scope.json exists — a run is in flight

Escape hatchARBEITSPLAN_DISABLE_GUARD=1, or close the run by removing the lock

matrizeWrite|Edit|MultiEdit

hooks/matrize_guard.py

Inert unlessthe configured design root exists (.design/ by default, or root: in .claude/matrize.local.md)

Escape hatchMATRIZE_DISABLE_GUARD=1, or enforcement: off in .claude/matrize.local.md

Two details matter beyond the cards above. First, andon's matcher covers Write and Edit only — it does not list MultiEdit, unlike befund's matcher on the same three tool names. Second, three matchers reach upstream of the edit itself by covering Skill|Task|Agent, so they can intercept a dispatch and not only a file write: takt and arbeitsplan share the identical six-token matcher Skill|Task|Agent|Write|Edit|MultiEdit, and cupertino reaches the same three dispatch tools plus Write|Edit|Bash in place of MultiEdit. The division of labour between takt and arbeitsplan is deliberate rather than incidental: takt answers "has the required step run?", which is legitimately repo-level state, while arbeitsplan answers "did THIS in-flight dispatch issue this edit?", which is the per-dispatch attribution problem below. Ordering appears in exactly one of the two. cupertino uses that reach for its own ordering — refusing cupertino-focus, cupertino-longevity, cupertino-integrate, or cupertino-council before cupertino-backwards has run, via GATED_AFTER_BACKWARDS. takt's matcher ties arbeitsplan's as the widest of the nine — both add MultiEdit on top of the same three dispatch tools — and takt gates declared beat order across plugins rather than within one. nacharbeit is the only one besides zeugnis and cupertino that watches Bash, and for one reason: while its fix lock is open, a git commit, push, reset or checkout from inside the pass is refused, so a half-applied rework is never committed by the thing applying it. The remaining four — andon, lehre, matrize, befund — reach only the write tools (Write/Edit, MultiEdit on three of the four).

cupertino's guard changed shape in two ways ​

It denies on first use. The hook used to be inert in any repo with no .cupertino/ directory, so cupertino-focus, cupertino-longevity, cupertino-integrate and cupertino-council ran on their first call with no cupertino-backwards before them. It now denies those four skills there, with a reason that names cupertino-repair for the case where the work is changing something that already exists. The "inert unless" line on its card is still true for every other tool; it is no longer true for these four skills. A session that used to reach cupertino-council straight away now has to run cupertino-backwards first or use the escape hatch.

During a repair run it polices the whole session, not only cupertino. While .cupertino/repair/ACTIVE exists, every Write, Edit, MultiEdit and NotebookEdit is limited to the paths of the one active approved item, shell writes and deletes (rm, mv, sed -i, redirects) and git mutations are denied, and cupertino-council, -elevate, -unbox and -reveal are denied. The payload carries no field naming which plugin issued an edit (see the next section), so the guard cannot exempt anyone else: an andon-loop remediation, a nacharbeit-fix pass, a befund-idiom-fix cluster or a plain direct edit made in the same session is denied until the run ends with repair_state.py end. Do not interleave them. Finish or abandon the repair first (repair_state.py end --abandon), or set CUPERTINO_DISABLE_GUARD=1 for the session, which also turns off the first-use denial above. Unlike befund's and zeugnis's guards, this one is gated on a run marker that a repair opens and closes, so it behaves like a per-dispatch lock for the duration of one run.

All nine fail closed, with one shared exception ​

Every one of these nine hooks fails closed: an unexpected internal error — a malformed JSON payload, a filesystem error, anything the hook did not anticipate — denies the tool call rather than silently allowing it, and the deny message always names the escape hatch. andon's own docstring states the reasoning plainly: a hook that fails open on its own bug is not an enforcement hook, and three of andon's own guards were once observed failing silently that way before this rule was adopted.

befund's and zeugnis's edit-scope guards carve out exactly one shared exception to that rule: if the plugin's own shared scripts/lib/ package is missing or broken at import time, the hook degrades to a single stderr warning plus an allow, not a deny. The reasoning both docstrings give, nearly word for word: a packaging defect in the plugin itself is not evidence that the edit under review violates a rule, and blocking every future edit in every repository because of a broken import is a strictly worse failure than one missed enforcement check. andon's and cupertino's guards carry no such carve-out in their source — their fail-closed behavior has no documented exception.

Why a hook cannot tell whose edit it is ​

befund's guard states the constraint every one of these hooks has to design around, verbatim:

PreToolUse's payload carries no field identifying which agent/plugin issued the Edit/Write/MultiEdit.

The consequence is concrete, not theoretical: befund's own guard once gated on repo-level state — "does this repo look befund-managed" — and that swept every edit in the whole session, from any plugin or a direct user edit, into the gate the moment a repo merely had analysis/befund/ on disk, blocking zeugnis, cupertino, and passung remediators along with ordinary direct edits. The general rule that follows: gate on a per-dispatch lock, never on repo-level state, whenever the question a hook is answering is "did the currently-in-flight remediation issue this specific edit." befund's edit_scope.json, zeugnis's remediation_scope.json and arbeitsplan's run_scope.json all do exactly that now — opened immediately before a remediator agent is dispatched, holding the specific file(s) that dispatch is allowed to touch, and closed after. nacharbeit's fix_scope.json is the third and the widest-reaching, because its remediators edit other plugins' files: it lists every file the review touched with its fix tier, so the guard can refuse an opus- or human-tier file by name rather than as "unknown", and it is released only by a post-check script that has re-run every test and diffed every contract.

That rule does not extend to every hook in the table. andon's and cupertino's guards answer a different question — "is the ledger in a stop state" and "has the required ordering step already run" — which is legitimately repo-level state rather than a per-dispatch attribution problem. Only befund's, zeugnis's, nacharbeit's and arbeitsplan's edit-scope guards are solving the "whose edit is this" problem the quoted constraint describes, and only those three need a per-dispatch lock rather than a durable flag. lehre's, like andon's, answers a repo-level question ("does this write violate the declared doctrine, or run ahead of a unit that has not been validated"), which is why its marker is a durable ruleset rather than a lock.

Measured, not inferred: test/workflows/evidence/plan-file-under-lock.md, reproducible in under a second with bash test/workflows/reproduce_hazard.sh — no tokens, no agents, no network.

Plan mode permits exactly one write, the plan file at ~/.claude/plans/<name>.md. While an arbeitsplan run-scope lock is open, that write is denied, because the path resolves outside the repository the run was compiled against. A subagent under both therefore has nothing it may legally do, and it cannot resolve the conflict itself: it has neither ExitPlanMode nor AskUserQuestion.

It surfaced the expensive way before it was reproduced the cheap way. Three builder agents in one fan-out each returned measured: false, having correctly refused to bypass the guard — a whole batch spent on an environment conflict rather than on the work.

The same probe records a second denial worth knowing: during a fanout-redundant phase, a write is refused even when the lock's own writeScope names the path, because only the landing step may touch the shared tree. That is what makes a merge conflict structurally impossible in an arbeitsplan run.

Neither denial is a malfunction. Close the run, or plan outside it; reaching for ARBEITSPLAN_DISABLE_GUARD=1 disables a guard that is working.

Two diff baselines ​

The official security-guidance plugin computes its own baseline independently of whatever a controller session has computed for its own purposes. It runs git stash create at UserPromptSubmit to capture a baseline SHA, then reviews the diff against that baseline at the Stop hook — with no awareness of any BASE_SHA a controller recorded via git rev-parse HEAD before dispatching an implementer (the pattern subagent-driven-development uses for its own review packages). Two uncoordinated baselines exist in the same session whenever both are active: one the controller tracks for its task-review diffs, and one security-guidance tracks for its own Stop-hook review.

The plugin documents its own escape hatch for exactly this overlap: setting ENABLE_STOP_REVIEW=0 disables only the Stop-hook diff review — commit and push reviews stay active — and the plugin's own README names the intended case as "multi-agent / shared-worktree setups where another agent can move HEAD between a worker's turns." A session running subagent-driven-development's implementer/reviewer cycle alongside security-guidance is exactly that case.

Do not combine ​

learning-output-style instructs the model to stop at decision points and hand a small, meaningful piece of code (5-10 lines) to the human rather than writing it directly. That instruction is the direct opposite of subagent-driven-development's continuous-execution premise, which names only four conditions that should stop a running plan — an irreversible or destructive operation, a security-sensitive action, a side effect outside the worktree, or a plan broken enough that every path forward is a guess — and explicitly rejects "should I continue?" prompts between tasks as a waste of the human partner's time. Running both in the same session means one instruction set pauses for contributions the other set was told never to pause for.

Name collisions ​

code-simplifier ships as an agent twice: once in its own plugin, and once again inside pr-review-toolkit. Both declare the same agent name (code-simplifier), the same model: opus, and a byte-identical system-prompt body — the only difference is the frontmatter description field's format (a short one-line description in the standalone plugin, a longer description with worked examples in the pr-review-toolkit copy). Installing both plugins yields two agent definitions sharing one name; nothing in either plugin's manifest declares which one a bare dispatch by that name resolves to.

A second, less certain collision: code-modernization and befund both write a file named MODERNIZATION_BRIEF.md with different schemas — befund's transform brief and code-modernization's own brief are not interchangeable documents that happen to share a filename. befund writes to its own output directory (default analysis/befund/, resolved through befund_cli.py resolve-output-path); code-modernization's modernize-brief command writes to analysis/$1/ MODERNIZATION_BRIEF.md, where $1 is a target name supplied to the command. The two only collide if a session configures both to write into the same analysis/ subdirectory. This is presented as an unverified hazard rather than a confirmed conflict: no collision guard exists in either plugin's write path, but no test or run was found that actually forces the two into the same directory to prove the collision happens.

Ambient, not invoked ​

Several plugins in the official set are never dispatched the way an agent or skill is — they run continuously in the background for the whole session instead of being a participant a controller adds to a workstream:

  • security-guidance installs SessionStart, UserPromptSubmit, PostToolUse, and Stop hooks that fire on every matching event for the life of the session, not on request.
  • The two output-style plugins, learning-output-style and explanatory-output-style, inject their instructions once at SessionStart and then shape every response for the rest of the session — there is no per-task moment where either one is "used."
  • The twelve LSP plugins (clangd-lsp, csharp-lsp, gopls-lsp, jdtls-lsp, kotlin-lsp, lua-lsp, php-lsp, pyright-lsp, ruby-lsp, rust-analyzer-lsp, swift-lsp, typescript-lsp) supply language-server diagnostics as a standing capability available to whatever agent happens to be running, rather than appearing as a workstream of their own. A reviewer dispatched into a repository with the matching LSP plugin installed sees sharper diagnostics as a precondition of the environment it runs in — the LSP plugin is not itself a gate a fix has to pass, and it is never a defender, challenger, or verifier a controller names in a dispatch.