Hooks and guards
Everything on this page is off until you turn it on — and most of it advises rather than denies.
What it is#
Guards are checks you can run by hand, from a git hook, or from an IDE hook:
| Command | What it denies |
|---|---|
orch guard dup <file> |
A file duplicating a shared symbol; also reports copied function bodies |
orch guard code-lang [root] |
Identifiers and user-visible strings that do not match workspace.code_lang |
orch guard lang [root] |
Docs that do not match workspace.docs_lang |
orch guard ddl [files...] |
A migration or CREATE TABLE change with no seeder or repository companion |
orch guard tracker [path] |
A TRACKER state table that breaks T1–T4: bad status, SPEC with no .md, VERIFIED with no evidence, narrative dump |
There are more: comment, lock, add-all, shared-brand, control-bytes, artifacts, encoding,
plus six guards that ship on by default in the shared/strict profiles: docs, signature,
upsert, env-drift, shared-shadow, template-drift.
.orch/gates/user/manifest.json lets a workshop add its own deny/require/cmd checks; they run at
the same sealing, git-hook and IDE-hook points as the guards above (orch guard rules). The
Chair's own code-file writes are capped too — a 3rd write gets an advisory naming
dispatch_worker, a 4th is denied in shared/strict profiles; see the
how-to guide for the full rule. Any guard that blocks fails the
dispatched task and makes worktree_promote refuse it.
Hooks are entry points into your harness, each gated by its own consent key in orch.yaml:
| Key | Fires on | What it does |
|---|---|---|
context.hooks.post_tool |
Claude Code PostToolUse, OpenCode tool.execute.after |
Shrinks a tool result before it lands in the context window |
context.hooks.post_tool |
cursor-agent postToolUse (Shell, Read, Grep) |
Cannot shrink the result — answers with additional_context naming the Orchemax tool that would have cost less |
context.hooks.pre_compact |
Claude Code PreCompact |
Runs the orch memory condense condenser on workshop memory before the harness compacts |
guard.dup.mode |
Claude Code PostToolUse and cursor-agent postToolUse, both on Write/Edit |
Runs orch guard dup on the file that just landed and returns the deny text as context. Never denies the write — see below |
The context.* keys default to false. The write-lane duplicate gate is not one of them: it rides on guard.dup.mode, which is advisory out of the box, and it is installed by the same orch guard wire regardless of the context consent.
The duplicate gate on the write lane#
Cycle 7 of the R5 end-to-end run dispatched five real workers and the duplicate gate ran on none of their writes: orch guard wire --cursor installs a shell hook, and a Cursor Write never touches a shell. That is fixed in three layers, and only the first depends on the CLI having hooks at all:
- On the write — Claude Code and cursor-agent get the deny text as
additionalContext/additional_contextwhile the agent is still in its loop. - At verification — every dispatched worker, hooks or not, including Copilot CLI and agy, whose harnesses Orchemax cannot wire.
guard.dup: advisorysealsguardrails: warn;blocksealsguardrails: failand the dispatch answersstatus: failedwith the deny lines aserror_head. - At promote —
worktree_promoterefuses a session whose verification carries a deny.
The scan also reads the worktree of every other running session, so two workers writing the same block at the same moment are caught while both are in flight.
What each CLI allows#
A hook can only save what its harness lets it replace, so the honest answer differs per CLI:
| CLI | Hook surface | Can it replace a tool result? |
|---|---|---|
| Claude Code | PostToolUse, PreCompact (~/.claude/settings.json) |
Yes — hookSpecificOutput.updatedToolOutput, as long as the replacement keeps the tool's own output shape |
| OpenCode | tool.execute.after plugin |
Yes |
| cursor-agent | project .cursor/hooks.json (beforeShellExecution for the guard, postToolUse for context) |
No, not for Shell/Read. Its only replacement field is updated_mcp_tool_output, documented "For MCP tools only", and a Read payload carries content_length rather than the file body. What a hook can do is inject additional_context, which arrives as a <system_reminder> right after the tool result |
| Copilot CLI | hooks ship inside plugins (copilot plugin), no project hooks file |
Not wired by Orchemax |
| agy | no hook mechanism (agy --help) |
No — compression there is skill-driven only |
| Codex CLI | — | Cannot replace tool output at all |
For the CLIs with no wiring path, the lever is the orch-worker skill: it opens with the read rules (code_snippet over opening a file, trace_path/impact for callers, context_externalize for bulk output), and usage_status {view: "workers"} reports orch_read_tools per session so adoption is a measured count rather than a claim.
Wiring:
orch guard wire # from guard.profile, or --git / --claude / --cursor
orch guard unwire # add --git to remove the git hooks too
orch guard hooks install # git pre-commit + pre-push, orch-marked
orch guard hooks status
orch guard profile # show, or set: lite | shared | strict | bring | none
The dangling-reference guard#
At sealing, Orchemax reads every path the change set deleted or renamed away and greps the rest of
the tracked tree for it — the full relative path, or its dir/basename shape. A hit on a
code, deploy, or config line fails the seal, listing file:line → old path; a hit on a
comment-only line or inside a .md doc is advisory only. This is exactly what broke a founder's
deploy once: deploy.ps1 still shelled out to a script deleted by hand, and nothing said so
until the deploy ran. orch harness scan reports the same shape for paths git can no longer
find on disk (deleted, not yet git rm'd) that a tracked file still calls, and separately flags
a core.hooksPath pointing at a directory that no longer exists — hooks configured there stop
firing the moment the directory goes away, silently.
Overriding one finding#
A gate wrong about one line should not cost you the twenty that were right. git commit --no-verify
skips every gate at once and records nothing, so there is a narrower way out: one marker, on the
offending line or alone on the line directly above it.
secret := loadFixtureKey() // orch:allow secrets — rotated key, kept for the replay test
The marker is matched as a substring, so #, --, //, <!-- -->, { } and <# #> all carry it
in a polyglot workshop. Four rules, each enforced by orch guard allow:
| Rule | Why |
|---|---|
The gate is named as orch guard exposes it. Waivable: comment-prose, litter, minimal-code, secrets, signature |
Heuristics and taste calls, where a wrong deny is the gate's own fault |
reproduce, ddl, dup, code-lang, lang and control-bytes are not waivable |
A marker there is the user lying to themselves about the change, not about the gate |
| A reason of at least 12 characters is mandatory | A marker with no reason silences nothing and is itself a finding, which is what keeps the hatch from becoming a habit |
One line only: the offending line, or alone on the line above. A litter finding names a file's existence, so its anchor is that file's first two lines |
Never a whole file, never a folder — an override is per occurrence |
The marker is honoured where the gates actually gate: the git hooks and the worker seal. One verb
run by hand (orch guard secrets x.go) still lists the waived finding — a report that gates
nothing over-reports rather than hide, and orch guard allow is what answers "is my marker good".
Every honoured marker lands in guard_events with verdict override, and orch stats prints
overrides by gate. That count is the point: a gate people keep waiving is a gate to tune, and
the number says which one instead of leaving it to anecdote.
New in 0.5.0: gates at the moment of action#
Every tool call is judged by hooks, and a worker's commit runs the same gates the final seal runs: generated outputs unstaged, ledger edits, removed tests, unlocked shared writes and red-first. A worker learns of a mistake in seconds instead of after a full seal; the seal stays as the backstop.
A command that would wait on stdin forever — a bare interpreter or REPL, an empty heredoc — is refused for every seat, with the fix in one line. See Release 0.5.0.
Advisory or block#
The distinction is per guard.
- Advisory — the clone scan, by default.
guard.dup.mode: advisoryreports what it found and injects it as context; the write proceeds. Setblockand the same finding fails the call withclone guard denied N duplicated block(s); setoffand it does not look. - Block — every other guard. A finding prints to stderr and the command exits non-zero, so a pre-commit hook stops the commit.
- Soft-skip — outside a bound workshop, or when the relevant setting is unset.
orch guard langwith noworkspace.docs_langskips rather than guessing.
Why it exists#
Field lessons, each cheap to state and expensive to learn. git add -A on a shared layer steals another session's work in progress. Chatting with a model in one language quietly puts that language into identifiers and error strings. A migration without a seeder passes review and fails in production. None of these need a policy engine — they need one check at the moment the mistake is made.
They are opt-in because the workshop is yours: orch init calls the starter kit recommendations, not mandates.
How it relates to the rest#
- The same guardrail suite runs twice: from the pre-commit hook you installed, and again when a session closes, over exactly the files that session touched. That second run is the observed half of the verification report.
orch harness scanreports which AI tools and doctrine surfaces it can see, before you decide what to wire.- Guard profiles control what
orch initwires:liteseeds docs and skills with no hooks,noneskips the doctrine seeds entirely.
Nothing to run here#
Nothing here runs unless you wire it. Run the guards by hand if that is all you want; orch guard wire is the switch that enforces them.