Hooks and guards
Opt into context hooks that shrink tool output, and run the local guardrail checks that keep a shared workshop sane.
Goal#
Turn on the two opt-in harness context hooks (both default off), install and
run orch guard checks, and pick a guard.profile that decides how much of
this Orchemax wires up for you.
Context hooks (opt-in, off by default)#
orch.yaml:
context:
hooks:
post_tool: true # Claude Code PostToolUse / OpenCode tool.execute.after
pre_compact: true # Claude Code PreCompact
post_toolshrinks an oversized tool result before it reaches the model (orch hook post-tool), using the same crush/externalize settings ascontext.guidance. It skips any tool name listed incontext.guidance.protect_tools(default:Bash,PowerShell,Read,Grep,Glob,Edit,Write,Agent,Task,WebFetch,Shell,bash,read_file,write_file— coding tools where a lossy rewrite would hide a real result).pre_compactruns the same condenser asorch memory condenseon.workflow/BOARD.mdand.orch/LAST_SESSION.mdjust before the harness's native compaction discards the transcript — so the board's## Clarifiedsection and open checkboxes survive byte-for-byte instead of being summarized away.
Neither hook does anything on its own: both need
orch guard wire --claude (or --profile) to actually install them into
~/.claude/settings.json / the OpenCode plugin folder.
orch guard checks#
orch guard dup <file> # deny/warn on a file that duplicates a shared symbol or clones a function body
orch guard code-lang [root] # identifiers/strings must match workspace.code_lang
orch guard lang [root] # docs must match workspace.docs_lang
orch guard ddl [files...] # migrations/CREATE TABLE need a seeder/repository companion
orch guard tracker [TRACKER.md] # TRACKER.md status table sanity (T1–T4)
orch guard lock <path> --holder # deny editing shared/** without a valid lock
orch guard dup is advisory by default — it prints where the existing
copy lives and lets the write proceed. Set guard.dup.mode: block in
orch.yaml to deny the write instead (off disables the scan entirely).
guard.dup.window / min_tokens / max_files tune the clone scan.
A blocking guard — dup in block mode, an unlocked shared/ write, a DDL
pairing miss, a docs/code language deny, a user rule with severity: block,
or one of the six genesis guards when guard.profile makes it block — fails
the dispatched task and makes worktree_promote refuse it, naming the guard
that denied; advisories and warn-severity findings only annotate the report
and never change the task's status.
orch guard tracker and its sealing-time "code staged without TRACKER.md"
reminder are opt-in: they say nothing until the project already has a
TRACKER.md somewhere in the tree, or guard.tracker: true is set in
orch.yaml. A fresh workspace that never adopted the convention gets no
tracker note anywhere — hooks, guard hooks run, or sealing.
orch guard lang --count and orch guard code-lang --count walk the tree
and print one rule(lang) hits sample_path:line row per detected language
without ever denying (exit 0 always) — run one before switching an existing
codebase to guard.profile: strict, which runs the same count itself and
refuses without --yes when it finds any hits.
Natural languages#
workspace.docs_lang and workspace.code_lang accept any of these codes (also
xx-YY, e.g. es-US); an unsupported code skips the guard with a note
instead of failing.
| Code | Language | lang flags prose in… |
code-lang flags identifiers/strings in… |
|---|---|---|---|
en |
English | any other supported language | any other supported language |
es |
Spanish | any other supported language | any other supported language |
pt |
Portuguese | any other supported language | any other supported language |
fr |
French | any other supported language | any other supported language |
de |
German | any other supported language | any other supported language |
it |
Italian | any other supported language | any other supported language |
English is never flagged inside identifiers for a non-English
code_lang (library APIs and language keywords are English by convention) —
set guard.code_lang_strict: true to flag it there too. String literals
are always checked against every supported language, English included, since
a shipping string in the wrong language is a product bug either way.
orch guard comment-prose [path] is advisory in every profile — it never
denies. Per code file it notes comment blocks of 6+ consecutive lines (the
file header, package/module doc, license blocks and generated files are
exempt) and any block carrying a narrative marker (an ISO date, "before the
fix", "used to", "we saw", "the founder", "historically"). It runs as a note
in orch guard hooks run and every verification report; --count prints one
summary row instead of every hit.
At sealing (a dispatched worker's verification), code changed without any
test/spec file in the same diff gets an advisory note tests: none touched
— never a failure, and silent when the only files that changed are
docs/config.
Gate-silla: when the seat driving orch guard ide-hook is the Chair
(ORCH_ROLE=chair, or no role at all with a Chair already running for this
workspace), it counts the Chair's own code-file writes for the session. The
3rd write gets an advisory naming dispatch_worker; the 4th and later are
denied in shared/strict (advisory-only in lite, off in none).
ORCH_GUARD_CHAIR_WRITES=0 escapes it for the rest of the session.
Sandbox and litter#
orch guard litter [files...] denies a new file or dot-directory that
reads as agent scratch instead of project code — editing an existing file is
never litter. The same shapes are judged at the IDE's write hook, at sealing,
and (read-only) by orch harness scan / workspace clean for what is
already on disk.
Name and suffix rules#
A basename is judged together with its extension, so ordinary source is never caught:
| Basename shape | Doc-shaped ext (.md .txt .log .out .rtf .csv .json .html), or no extension outside bin//scripts/ |
Any other extension |
|---|---|---|
Bare proof/scratch stem — test(s) tmp temp scratch verify check e2e gate audit debug dump poc draft note(s) report transcript consulta (e.g. report.md, bare scratch) |
litter | never litter (report.go, check.py, verify.ts, tmp.go) |
Same stem + more words (gate-x.sh, verify_login.sh, e2e-smoke.sh) |
litter | litter, unless the extension is a compiled/module/config one (.go .rs .c .cs .java .tsx .jsx .vue .svelte .php .dart .sql .proto .py .ts .js .mjs .cjs .rb .pl .lua .yaml .yml .toml .ini .cfg .json … — chair decision, normal code is never denied by name; shell/batch extensions .sh .ps1 .cmd .bat are deliberately excluded, so verify_login.sh stays litter, and an unreferenced .py/.js/.ts/.rb/.pl file is instead judged by the unreferenced-script rule below) |
Suffix markers stack on top of the name rule:
| Suffix | Extension | Litter? |
|---|---|---|
-final -copy -backup -bak -orig -rej -swp |
any | yes |
(1), (2), … (OS-generated copy) |
any | yes |
-v2/_v2/.v2, -new, -old |
doc-shaped only | yes |
-v2/_v2/.v2, -new, -old |
code/schema (.go, .sql, .proto, …) |
no — an ordinary version, not a copy |
Duplicates#
- A new file whose bytes exactly match an already-tracked file of the same
size is litter —
duplicate of <path>(same-size files are the only ones ever read).orch harness scan/workspace clean's whole-tree pass runs this only inside a git repository, and only against untracked files of 64 bytes or larger — a tracked duplicate is the user's decision, and a same-size match below that floor is too likely to be coincidence. - A new doc under the docs folder whose stem (the suffixes above stripped
first) or first
#heading matches an existing tracked doc is litter —update the canonical doc <path>.
Unreferenced new scripts#
At sealing, a brand-new script-like file (.sh .ps1 .cmd .bat, or any new
file carrying the Unix executable bit) that nothing in the change set or the
tracked tree names by path is litter — move it to .sandbox/<seat>/ or wire
it into deploy/Makefile/CI. .py .js .ts .rb .pl get a second way to pass: a
tracked sibling with the same extension already sitting in that directory, or
the file's own stem appearing as a whole word (import-style) somewhere else
in the tree. Framework entry-point names (index.*, main.*, __init__.py,
*.config.*, *.d.ts, …) never need a reference at all.
Dot-directories#
Only .sandbox/, .orch/, .git/, .cursor/rules/, .github/, and the
files .claude/settings.local.json (and the legacy .claude/settings.json),
.gitignore, .gitattributes, .editorconfig, .mcp.json are wired by
Orchemax itself. Every other new
dot-directory is denied by design — an agent cannot create .vscode/,
.devcontainer/, .husky/, .storybook/, or any other tool folder that
starts with a dot. If you want one of those in the tree, commit it yourself
by hand (an existing file that predates Orchemax's own wiring, or whose last
commit is not by an orch-<seat> identity, is never reported by
harness scan/workspace clean), or list its path glob in the project's
keep config.
Shell write gate#
The same verdict covers the shell: the Bash/PowerShell PreToolUse hook
(and cursor's beforeShellExecution) reads the command line for the paths it
would create — >/>>, cat > x <<EOF, tee, cp/mv destinations,
Out-File, Set-Content, Add-Content, New-Item, Copy-Item,
Move-Item — resolves them against the shell's cwd, and denies a litter
target or a Windows device name (> nul, which leaves a literal undeletable
file; discard with >/dev/null or $null), while /dev/null, $null,
NUL: and a > inside quotes are left alone.
Severity, the sandbox, and "yours, not litter"#
Severity follows guard.profile: deny at the IDE PreToolUse hook and at
sealing in lite/shared/strict, advisory-only in bring, off in none.
orch harness scan and orch workspace clean never report an existing
tracked file as litter when any of these hold:
| Exemption | Condition |
|---|---|
| referenced | another tracked text file names it, by path or dir/basename |
| pre-Orchemax or human | tracked, and either added before this project's own Orchemax wiring, or its last commit's author is not orch-<seat> |
| kept | its path matches a projects[].keep glob (** supported) in orch.yaml |
An untracked file only ever gets the first or third exemption — never the
second, since it has no commit history to check. Every commit a dispatched
worker makes carries GIT_AUTHOR_NAME/GIT_COMMITTER_NAME = orch-<seat> and
email <seat>@orch.local, so that check is a real identity, not a guessed
trailer. A seat/worker name itself must match
[A-Za-z0-9][A-Za-z0-9._-]{0,63} (1–64 characters, no separator, no leading
dot) and cannot be a Windows device name — rejected before dispatch, since it
becomes a path segment under .sandbox/.
orch project add seeds <project>/.sandbox/README.md and marks
.sandbox/ in the project's .gitignore. At each task's own close, any file
that task created under its seat matching a tmp/temp prefix or ending
.log/.out/.tmp is removed right away; everything else in
.sandbox/<seat>/ waits for the age sweep once it is older than
guard.sandbox_days (default 14) — which never removes README.md, never
removes a directory, and never follows a symlink or junction out of the
sandbox. orch workspace clean --dry-run lists each seat's sandbox folder
with its size. orch harness scan lists litter already on disk (count and
first 10 paths) and never deletes it.
The rule in words, for any agent working under Orchemax: anything that is
not product code goes to .sandbox/<seat>/; temp files there are deleted at
task close, not left for the sweep. Never create -v2/-final/copy files —
update the canonical one instead. Never > nul; use /dev/null or $null.
Pipe build logs to .sandbox/<seat>/ too (tee build.log in the tracked
tree is denied the same way any other litter write is). A script you invoke
from the shell is not inspected for what it writes inside — the hook only
sees the command line, not the script body — so the same rule still applies:
write its output under .sandbox/<seat>/, on your own discipline.
Dangling references#
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
shorter dir/basename shape — matched only at a real path-segment boundary,
so db/client.go never fires on a deleted mongodb/client.go, and moving
main.go to cmd/app/main.go with the Makefile updated to match is not a
false hit. A hit on a code/deploy/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. 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. Severity follows the same guard.profile ladder as
litter: advisory in bring, off in none.
Steps#
orch setup already offers to wire the project scope once, right after it
binds the workspace (default yes; --yes accepts, never touches user scope;
--allow-user-scope is needed for agy's machine-global MCP entry). The steps
below are for wiring, rewiring, or picking a different profile by hand.
-
Pick a profile (or accept the recommended default):
orch guard profile lite # docs + skills; no hooks (recommended day-1) orch guard profile shared # + git pre-commit/pre-push on shared/project repos orch guard profile strict # + Claude user-hook wire, locale/tracker awareness orch guard profile bring # keep your own CLAUDE/rules; orch only adds locks/symbols orch guard profile none # orch.yaml only -
Wire the adapters for that profile:
orch guard wire --profileOr wire specific adapters directly:
--git,--claude,--cursor,--copilot. Every wire is project scope by default:--claudewrites/merges<workspace>/.claude/settings.local.json— git-ignored, the entry is added to the project's.gitignoreon first wire — and never opens~/.claude/settings.json. To wire the user scope — the settings every Claude Code session on the machine reads, plus agy's machine-global MCP config and the OpenCode plugin — add--scope user; Orchemax then asks you to confirm, naming the file, the Orchemax-marked block, the backup (settings.json.bak-<yyyymmdd-hhmmss>) and the undo command. In a script,--allow-user-scopeis that consent; a blanket--yesis not.--copilotis a no-op: a real headlesscopilot -prun never loads.github/hooks/*.json(verified live against the installed CLI) — the interactive folder-trust confirmation that gates it never happens headlessly, so Orchemax refuses to write a hook that would only ever look armed. Copilot stays covered by sealing (the guardrail check every worker's diff still gets when its session closes), same as an agent with no real-time hook at all.orch harness scanreports every scope (claudeuser,claude.project,copilot) as wired or sealing-only —copilotis always sealing-only today. -
Install git hooks directly (independent of
guard wire):orch guard hooks install [repo] orch guard hooks status [repo] orch guard hooks run [repo] # what the installed hooks actually call
Verify#
orch guard profile
orch guard hooks status
Undo#
orch guard unwire # removes the orch-marked block from both scopes (backup kept)
orch guard unwire --git # also removes git pre-commit/pre-push
orch guard unwire --project # project scope only — no user-scope file is opened
orch guard hooks uninstall [repo]
Related#
your-own-rules.md— the checks that are yours, run where these run.memory-and-ledger.md— whatpre_compactcondenses.troubleshooting.md— "hook did nothing" when the workspace isn't registered.