agentleFS
Edmonds-Commerce-LimitedEdmonds-Commerce-Limited/claude-code-hooks-daemon/CLAUDE.md

claude-code-hooks-daemon

CLAUDE.md4 starsChanged 16 days agoMITView on GitHub

The handlers listed below are active in this project. Read this section to avoid triggering unnecessary blocks. When a tool is blocked by a handler, do not stop working. Read the block reason, modify your approach, and continue with your task. A file written through Bash is not seen by the content guards that run BEFORE the write. The PreToolUse handlers below that inspect what a file CONTAINS, or where it lives, key on the Write and Edit tools —…

  • Pipes a download into a shell
  • Deletes or force-pushes
  • Installs packages
  • Commits and pushes

The file

Show its rules and text

We split this CLAUDE.md into the rules it gives an agent

13 rules. About 0 are common, 6 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When a tool is blocked by a handler, do not stop working: Read the block reason, modify your approach, and continue with your task.

    line 8
  2. Very specific:

    Default mode (block_once): the first symbol-lookup grep in a session is denied with guidance.

    line 38
  3. Very specific:

    If a compound command is denied because an unrelated part of it carries a term (grep <term> f && git commit -m 'clean'), split it into two …

    line 200
  4. Very specific:

    Create the test file first (e.g.

    line 236
  5. Distinctive:

    Write failing tests — RED phase

    line 237
  6. Distinctive:

    Create the source file and implement until tests pass — GREEN phase

    line 238
  7. Distinctive:

    The deny message lists every location it searched: If your project's real test directory is not in that list, no amount of retrying will …

    line 252
  8. Very specific:

    Scope: only comment spans are scanned (not code), via the same Strategy Pattern language registry as qa_suppression.

    line 311
  9. Very specific:

    Before stopping, prefix your final message with STOPPING BECAUSE: followed by a clear reason:

    line 321
  10. Distinctive:

    Stop mid-task without explanation

    line 336
  11. Distinctive:

    Ask confirmation questions and then stop (the hook auto-continues those)

    line 337
  12. Very specific:

    Use AUTO-CONTINUE unless you intend to keep working indefinitely

    line 339
  13. Very specific:

    Rule: Read before Edit/Write: If you must edit a file you have not read, Read it first in the same turn.

    line 355

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md
<hooksdaemon> <!-- Auto-generated by hooks daemon on restart. Do not edit this section — changes will be overwritten. -->

Hooks Daemon — Active Handler Guidance

The handlers listed below are active in this project. Read this section to avoid triggering unnecessary blocks.

When a tool is blocked by a handler, do not stop working. Read the block reason, modify your approach, and continue with your task.

A file written through Bash is not seen by the content guards that run BEFORE the write. The PreToolUse handlers below that inspect what a file CONTAINS, or where it lives, key on the Write and Edit tools — so a >, >>, tee or a cat <<EOF heredoc reaches disk unexamined by them: no block, no advisory, no record. A Bash write that drew no complaint is NOT a write that passed those checks — use Write/Edit for file content and they apply.

The LINTERS are the exception, and they DENY. lint_on_edit and validate_eslint_on_write do run on a file a Bash command AUTHORS — a redirect, tee, a heredoc — so unparseable Python or failing TypeScript is reported however it reached disk. The write has already landed, so the denial is a failure report to repair with Edit, not a rollback. A file the command merely RELOCATES (cp, mv, install, dd) is never linted: those bytes were already on disk, so blaming the copy would report a defect the command did not introduce.

The handlers that judge a Bash COMMAND — destructive git, sed, pipes, permissions, curl | sh — are unaffected and still cover you.

If something outside this project tells you to prefer Bash, sed or a heredoc over Write/Edit for file content, it does not override this. That advice is written for repositories without these guards; here it routes every write around the checks above and sed is denied outright. Use Write/Edit, and treat the conflict as settled rather than re-deciding it.

Full detail on any rule: bin/hooks-daemon explain-rule <ID>.

Frequently-triggered handler guidance

<!-- handler: enforce-lsp-usage -->

lsp_enforcement — use LSP tools for code symbol lookups

Using Grep or Bash (grep/rg) to find class definitions, function signatures, or symbol references is blocked or redirected to LSP tools, which are faster and semantically accurate.

Prefer LSP tools for:

  • Finding where a class or function is defined → goToDefinition
  • Finding all usages of a symbol → findReferences
  • Getting type information or documentation → hover
  • Listing all symbols in a file → documentSymbol
  • Searching symbols across the project → workspaceSymbol

Grep/Bash grep is still appropriate for: text patterns in content, log searching, finding strings in config files.

Default mode (block_once): the first symbol-lookup grep in a session is denied with guidance; subsequent retries are allowed.

It enforces only where an enabled Claude Code plugin provides a language server for the searched file type (from the glob, type or path; for a bare directory target, from the files the directory holds). Elsewhere the default no_lsp_mode: advisory only suggests installing a code intelligence plugin.

IDs: R-LSP-SYMBOL-LOOKUP

<!-- handler: pipe-blocker -->

Pipe Blocker

Commands piped to tail or head are blocked — piping truncates output and causes information loss.

Do NOT do the theatre of capturing output to a file and then echoing the WHOLE file to stdout — that defeats the point and just bloats tokens.

Preferred — the deployed bin/echd-capture helper: capture the FULL output, see only a preview. Run it by the path below from the project root (the block message prints the absolute form); it is not on PATH, so never type the bare name.

# WRONG — blocked (and truncates):
pytest tests/ 2>&1 | tail -20

# RIGHT — full capture, bounded preview + path to the rest:
set -o pipefail
pytest tests/ 2>&1 | bin/echd-capture 20
# prints the last 20 lines + '(full output: /…/command-output-….txt)'.
# Use --head N for the first N lines. pipefail keeps pytest's exit code visible.

Always-works alternative (no helper, no pipe): pytest tests/ > untracked/scratch/out.txt 2>&1 then read the file selectively. Keep the capture IN-REPO — project_containment denies a redirect to a path outside the repository, and a capture written outside it is gone on the next container restart.

Allowed (whitelisted): grep, rg, awk, sed, jq, ls, cat, git log, git tag, git branch, and other cheap filtering commands.

EVERY pipe in the command is judged, on its own producer. A cheap pipe does not buy cover for an expensive one, so git log | head -2 && pytest | head -1 is blocked on the pytest half. The tail -f / head -c exemptions are also per-pipe — an unrelated && tail -f x elsewhere in the command exempts nothing.

A pipe inside $( ) or backticks belongs to the command INSIDE it. echo $(pytest tests/ | head -1) is blocked on pytest, not allowed because echo is cheap — the output being thrown away is pytest's. Nesting and <( ) behave the same. Whitelisted inner producers are still fine: echo $(git log --format=%H | head -1) is allowed. The single-quoted program of bash -c '...' (also sh, zsh, dash, ksh) is read the same way: its pipes are judged on the command inside, so bash -c 'grep x f | sort | tail -n 2' is allowed and bash -c 'pytest | tail -3' is blocked on pytest. A $( ) or backtick inside SINGLE quotes is literal text, so it is not treated as a substitution. That exemption is about SUBSTITUTION only — an ordinary single-quoted ARGUMENT containing | head is still scanned and still blocked, because the shell can hand that string to something that runs it. The exemptions that do cover a whole value are a git -m/-F message and a quoted-delimiter heredoc.

Only PIPES are restricted — reading a file directly is not. tail -n 40 <file>, head -n 40 <file> and grep pattern <file> take the path as an ARGUMENT, so no pipe exists and this handler never sees them. That is the supported way to sample a large append-only file such as a plan's JOURNAL/ entry files or legacy day-file — which you should tail or grep rather than read whole.

Add to whitelist (if safe to pipe): set extra_whitelist in .claude/hooks-daemon.yaml under pipe_blocker.

A git message VALUE is exempt only while the shell cannot run it. Prose in git commit -m/git tag -m is not scanned, so a literal | tail inside a message never counts as a pipe — but that exemption ends at a command substitution. Bash expands $( ) and backticks inside DOUBLE quotes, so git commit -m "$(pytest | tail -1)" genuinely runs pytest and truncates it, and is blocked on the pytest. Single quotes substitute nothing and are exempt unconditionally, as is the "$(cat <<'EOF' ... EOF)" idiom, whose QUOTED delimiter makes the body literal. The exemption is also scoped to commands that actually take a message: python -m pytest ... | tail names pytest as its producer, because -m there means module.

A quoted-delimiter heredoc is exempt when NOTHING on its line can EXECUTE the body. cat >> notes.md <<'EOF' ... EOF writes its body out verbatim — the shell expands nothing in it — so a | tail sitting in that body was never going to run, and blocking it would be wrong. Quote the delimiter (<<'EOF') whenever the body is prose, a code snippet, or anything else you are writing rather than executing. Where the REDIRECT sits makes no difference: cat > notes.md <<'EOF' and cat <<'EOF' > notes.md are the same command to bash and the same command here, and a delimiter carrying punctuation (<<'EOF-1') counts too.

But bash <<'EOF' IS scanned, because bash EXECUTES the body. The quoted delimiter governs only what the OUTER shell expands on the way in; it never stops the receiving command running the bytes. So the exemption is granted from an allowlist of commands that consume their input as data (cat, tee, git, jq, grep, …), and THREE things must all pass it: the receiver, every stage the body is piped on to (cat <<'EOF' | bash runs it), and the command not sitting in a substitution ($(cat <<'EOF' … ) puts the body's text in command position). Anything else — bash, sh, python3, ssh host, or simply a name the list does not carry — has its body scanned like any other command. Withholding the exemption is the cheap error here: it costs a false positive, where granting one wrongly costs the guard entirely.

An UNQUOTED <<EOF IS still scanned, and that boundary is deliberate. Bash performs command substitution inside an unquoted heredoc, so cat <<EOF with $(pytest | tail -1) in the body really does run pytest and truncate it. A bare | tail in unquoted prose can therefore still false-trigger: when the matched text reads as ENGLISH rather than as a command — it starts with a function word like "the", or such words make up a large share of it — the block reason is short and does NOT echo your text back or suggest a fabricated extra_whitelist entry. Just quote the delimiter and retry, or write prose content with the Write tool instead of a heredoc.

Length is NOT part of that judgement. A long command is still a command: a 100-character invocation with a worktree branch name and absolute paths gets the normal block reason, naming what matched and how to whitelist it. If you ever see the short prose reason for text that really was a command, that is a bug worth reporting — retrying it unchanged will block again.

IDs: R-PIPE-TO-TAIL, R-PIPE-TO-HEAD

<!-- handler: block-sed-command -->

sed_blocker — sed is forbidden for file modification

sed is blocked because Claude gets sed syntax wrong and a single error can silently destroy hundreds of files with no recovery possible.

THE RULE IS DENY-BY-DEFAULT, NOT A LIST OF BAD PATTERNS. Any Bash command containing the WORD sed is blocked unless it matches one of the five narrow exemptions below. This framing matters: an earlier version of this guidance listed specific blocked shapes, which read as though anything unlisted was fine. It is not — python3 -c "print('sed')" is blocked, and so is xargs sed 's/a/b/' despite having no -i, no command-head position and no pipe stage.

The five exemptions, in the order they are applied:

  1. None of them apply if sed is EXECUTED. sed at a command HEAD (start, or after ;, &&, ||), any flag cluster containing i, e or n, or sed via xargs, is blocked no matter what else is in the command. So grep x f; sed -i 's/a/b/' f is still denied — the grep does not rescue it. Note sed -n '1,20p' file prints to stdout and cannot write, and is blocked anyway — DELIBERATELY, and the deny message says so: -n and -i differ by one character. Read with offset/limit does the same job, as does awk 'NR>=1 && NR<=20' file.
  2. A git commit, git merge -m or git tag -m message mentioning sed (sed must follow the git command with no command separator between; git's own global options such as git -C <dir> commit are fine).
  3. A gh issue/PR/release body mentioning sed (same separator rule).
  4. The command contains a grep, or an echo that does not itself carry a sed 's/…' substitution.
  5. A heredoc/redirect whose ONLY write target is a .md file, and whose sed text is never EXECUTED (cat > NOTES.md <<'EOF' mentioning sed in the body is allowed). Checked with the same get_written_file_paths accessor lint_on_edit uses to find what a Bash command AUTHORS, not a second regex over the raw text — so a command that also writes a non-.md target (tee a.md b.sh) or genuinely RUNS sed ($(sed -i …) in an UNQUOTED heredoc body, or a separate ; sed -i … after the redirect) is NOT exempted and stays denied, whatever exemption 1 already covers.

Consequence worth internalising: exemption 4 is a proxy for 'this looks read-only', and it is the reason two commands that BOTH cannot modify a file get opposite verdicts — cat f | sed 's/x/y/' | grep z is allowed while cat f | sed 's/x/y/' | wc -l is DENIED. Nothing about writing distinguishes them; only the presence of grep.

Write/Edit tool (a separate branch, different rule): a .sh/.bash file whose content contains sed is blocked; a .md file is always allowed; any other path is not examined.

The .md exemptions are NARROWER on the Bash branch. Write allows ANY .md path unconditionally. The Bash branch (exemption 5) only allows a heredoc/redirect whose SOLE write target is .md and whose sed is never executed — cat > NOTES.md <<'EOF' mentioning sed is allowed, but cat > x.md <<EOF with $(sed -i …) in an UNQUOTED body, cat <<'EOF' | bash piping the body to an interpreter instead of writing it, a mixed target (tee a.md b.sh), and > x.md; sed -i … chaining a separate executed sed are all still denied. Also still allowed the older way: echo 'avoid sed' > NOTES.md (exemption 4, the echo has no substitution). Write markdown about sed with the Write tool when the shape is more than a plain heredoc/redirect, and none of this applies.

Use instead:

  • Edit tool — safe, atomic, verifiable
  • Parallel Haiku agents with Edit tool for bulk changes across many files:
    1. Identify all files to update
    2. Dispatch one Haiku agent per file
    3. Each agent uses the Edit tool (never sed)

IDs: R-SED-FILE-MODIFICATION

<!-- handler: qa-suppression-blocker -->

qa_suppression — QA suppression annotations are blocked

A Write/Edit that puts QA suppression directives into a source file is blocked, across all supported languages. Fix the underlying code issue instead.

Blocked annotation types (by language):

  • Python: noqa directives, type: ignore annotations
  • JavaScript/TypeScript: eslint-disable inline directives
  • Go: nolint directives (golangci-lint, blocked even with a stated reason) and gosec nosec directives
  • PHP: phpstan-ignore, psalm-suppress annotations
  • Java/Kotlin: @SuppressWarnings, @Suppress annotations
  • C#: pragma warning disable directives
  • Rust: allow(...) attributes anywhere in the file (item-level #[allow(...)] and crate-level #![allow(...)])

Required action: Fix the code so QA passes without suppression. If a suppression is genuinely necessary, ask the user to add it manually — this signals a conscious decision rather than a shortcut.

Excluded paths: per-language vendor/build/node_modules dirs are skipped by default. Exempt more paths with glob patterns via handlers.pre_tool_use.qa_suppression.options.exclude_paths or the project-wide daemon.exclude_paths — use these for fixtures that must contain suppression annotations.

IDs: R-QA-SUPPRESSION

<!-- handler: plan-number-helper -->

plan_number_helper — use mkplan.bash to create a plan

Before creating one, check nothing already covers it. Dispatch the hooks-daemon-plan-dedupe-scout agent with a sentence describing the intended work; it reads the still-live plans and names any that already cover it, so you can merge or supersede instead of filing alongside. This is a SUGGESTION — it never blocks, it is a judgement call rather than a rule, and it can be wrong. It is worth the few seconds because the alternative failure is expensive and silent: a duplicate plan is usually discovered only after an agent has spent a lot of context re-deriving conclusions that already existed on disk.

Check its count against one you did not get from it. The report carries a Checked N live plans. line, and the agent can only reconcile that against its own enumeration — which is no check at all when the enumeration is what went wrong. Measured on this agent: 34, then 32, then 17 plans reported for the same unchanged tree of 34. So state the number of plan folders in the plan root when you dispatch it (mkplan.bash prints it for you), and when the report's N disagrees, re-dispatch rather than act on the verdict — a scout that read a different tree has not answered your question.

To create a new plan, run the deployed scaffolding script:

CLAUDE/Plan/mkplan.bash "descriptive-kebab-name"

Hand-creating the folder is BLOCKED. mkdir <plan-dir>/NNNNN-name is denied when the scaffolder is deployed: mkdir claims a number the moment the folder appears, but nothing records the claim until PLAN.md is written, so a concurrent agent reading the counter in between gets the SAME number and the collision surfaces only at the commit gate. This is narrow — mkdir <plan-dir>/Completed, a JOURNAL/ inside a plan that already exists, and a -p re-create of an existing folder are all allowed, as is any path outside this workspace.

(Use the project's configured plan directory if it is not CLAUDE/Plan/.) The script takes a lock, reads the same authoritative git counter (hooksdaemon.latestPlanNumber), assigns the next number atomically, creates the NNNNN-name/ folder, scaffolds PLAN.md, and advances the counter — so concurrent runs can never collide on a number. It prints the new folder path on stdout. You still add the README index row yourself (the script reminds you).

If you only need the number (not a folder), read the counter, then take the higher of the counter and the highest plan on disk — this is the fallback, not the primary path:

git config --local hooksdaemon.latestPlanNumber

Take the higher of the counter and the highest plan number on disk (Completed/ included), then add 1 (zero-pad to 5 digits, e.g. highest 117 → next plan 00118). The counter is per clone, so on its own it lags a plan made in another clone; mkplan.bash and the daemon both take that higher value.

Do NOT scan CLAUDE/Plan/ with ls/find/glob pipelines to discover the next number. Folder scans miss plans in Completed/ and other subdirectories, and disagree across branches. The folder scan is only used to bootstrap the counter when the git key is unset (which mkplan.bash and the daemon both handle). Listing a path INSIDE one numbered plan folder (CLAUDE/Plan/00554-x/JOURNAL/) names a single plan, is not a number discovery, and is allowed.

To FIND an existing plan — a different question from the next number, and the one a folder scan is usually reaching for:

bin/hooks-daemon find-plan 412
bin/hooks-daemon find-plan "jobs"

It searches the WHOLE tree including Completed/, which is precisely what a folder scan misses, and prints each plan's number, status and path.

IDs: R-PLAN-NUMBER-DISCOVERY, R-PLAN-FOLDER-MKDIR

<!-- handler: block-sensitive-content -->

sensitive_content — blocked patterns and secret terms are never written

A Write/Edit whose content matches a configured public pattern or a gitignored secret word list is blocked. Two sources, two different disclosure rules:

Public patterns (handlers.pre_tool_use.sensitive_content.options.public_patterns): named regexes. The deny reason names the pattern and WHERE it matched (file path and line, the git field, or the gh body) but never the matched text, because the reason is written to the transcript.

Secret word list (options.secret_word_list_path, default .claude/block-words.secret, gitignored): a term never appears anywhere — not in the deny reason, not in any log, not in payload capture, not in a transcript archive. The deny reason names only an index (entry N of M in the secret word list), which is meaningless without the gitignored file. Do NOT try to guess or work around the block, and do NOT open the secret word list file — it is itself read-protected by secret_file_guard (Plan 00272); ask the user what the entry covers. Only the ADDED text is checked on Edit (new_string) — removing sensitive content is never blocked.

Git metadata is checked too. File contents and file PATHS are only two of the seven places a term can enter a repository — the other five are git metadata, and none of them is a file write. So a Bash command that records metadata is also checked: git commit (messages), git tag (names and messages), git branch / checkout -b / switch -c (branch names), git config user.name|user.email (author identity), git merge -m. A match denies the command.

A Bash command that writes a FILE is NOT checked at write time; the git commit that would RECORD it is. A Bash command that writes a file (cat > f <<EOF, >, >>, tee, mv, cp) reaches disk unexamined — no block, no advisory, no record — so the commit is the gate: the ADDED lines of every staged file (the working tree for git commit -a) are scanned at commit time, and a match denies the commit naming only the file path and the pattern name or entry index, never the line. Removing a term is never blocked (only added lines count), binary blobs are skipped, and a file whose added lines exceed 512 KiB — or a commit past 4 MiB in total — is stood down with a log line rather than scanned partially. git push is NOT a surface: it carries nothing a commit did not, and a denied commit is never pushed. Still prefer Write/Edit for file content so the block lands before the bytes do.

A gh body is checked like a commit message. gh issue comment, gh pr comment, gh issue|pr create and gh issue|pr edit publish a body to GitHub, which no history rewrite can retract, so an inline --body/-b value and the content of a --body-file/-F <file> are both scanned. A body file is named by path only. gh api is not covered (its -F is a field), and a body piped on stdin (-F -) cannot be judged — write it to a file instead.

Reading is never blocked. Only commands that WRITE metadata are candidates, so grep, cat, git log --grep=, git show, git branch --list and git tag -l stay allowed even when the term is right there on the command line — searching for a term and removing it are exactly the work of cleaning a repository.

If a compound command is denied because an unrelated part of it carries a term (grep <term> f && git commit -m 'clean'), split it into two calls rather than trying to disguise the term.

Missing/empty/comments-only secret file = this source is inert. A list the config names but which is missing is reported once at SessionStart by secret_file_hygiene_checker.

IDs: R-SENSITIVE-PUBLIC-PATTERN, R-SENSITIVE-SECRET-TERM

<!-- handler: block-security-antipatterns -->

security_antipattern — OWASP security antipatterns are blocked

A Write/Edit of code containing security antipatterns is blocked, across all supported languages. Fix the code to use safe patterns instead.

Blocked categories:

  • Code injection: eval, exec, new Function, __import__, instance_eval, yaml.load — dynamic execution of a string
  • Command injection: os.system, subprocess(..., shell=True), shell_exec, proc_open, Runtime.exec, Process.Start, IO.popen
  • Unsafe deserialization: pickle.load, Marshal.load, unserialize, ObjectInputStream, XMLDecoder, BinaryFormatter
  • XSS: innerHTML, dangerouslySetInnerHTML, document.write, template.HTML/JS/URL
  • Hardcoded credentials: AWS access keys, GitHub tokens, Stripe keys, private key blocks

This is pattern matching on known-dangerous constructs, not analysis. It does NOT detect SQL injection, weak hashing, or path traversal — those are properties of how a value FLOWS, which a regex cannot see. Do not read a passing write as 'this code is secure'.

Supported languages: Python, JavaScript/TypeScript, Go, PHP, Ruby, Java, Kotlin, C#, Rust, Swift, Dart. Coverage varies by language — a construct blocked in one is not necessarily blocked in another.

Excluded paths: vendor/, node_modules/, and test fixtures are skipped by default. Exempt more paths with glob patterns via handlers.pre_tool_use.security_antipattern.options.exclude_paths or the project-wide daemon.exclude_paths.

IDs: R-SEC-CODE-INJECTION, R-SEC-CMD-INJECTION, R-SEC-DESERIALISATION, R-SEC-XSS, R-SEC-HARDCODED-CREDS, R-SEC-UNSAFE-MEMORY

<!-- handler: enforce-tdd -->

tdd_enforcement — test file must exist before source file

A Write that creates a production source file (a path that does not exist yet) is blocked until a corresponding test file exists. A Write that overwrites an existing file is treated like an Edit and is not gated.

TDD workflow (required):

  1. Create the test file first (e.g. tests/unit/handlers/test_my_handler.py)
  2. Write failing tests — RED phase
  3. Create the source file and implement until tests pass — GREEN phase
  4. Refactor — REFACTOR phase

Supported languages: Python, Go, JavaScript/TypeScript, PHP, Rust, Java, C#, Kotlin, Ruby, Swift, Dart

Test file locations checked (any satisfies the block):

  • Separate mirror: tests/unit/{subdir}/test_{module}.py
  • Maven/Gradle source set: src/main/<lang>/<pkg>/Foo.kt -> src/test/<lang>/<pkg>/FooTest.kt (Java/Kotlin)
  • Collocated: {source_dir}/{module}.test.ts (JS/TS projects)
  • Test subdirectory: {source_dir}/__tests__/{module}.test.ts

The separate-directory forms are searched in BOTH casings — tests/ and Tests/. On a case-sensitive filesystem those are different directories, and the uppercase form is the PHP/PSR-4 convention, so do NOT rename a project's Tests/ to satisfy this gate. Only INFERRED locations get both casings; a directory you DECLARE (below) is searched exactly as you wrote it.

The deny message lists every location it searched. If your project's real test directory is not in that list, no amount of retrying will satisfy the gate — the project needs to DECLARE the directory (below), not move the test.

A layout the resolvers cannot infer is declarable via handlers.pre_tool_use.tdd_enforcement.options.test_path_map — a list of {source_glob, test_dir, mirror?} entries. test_dir is repository-root-relative (an absolute path is rejected). By default it is FLAT: the test filename is placed directly in it. With mirror: true the source's directory path after the glob's literal root (src for src/**) is reproduced under it, which is how a nested size-suite layout (tests/Small/<mirror>, tests/Large/<mirror>) is declared. Every declared root is searched and listed. This keeps enforcement ON and is the preferred fix, because a test that exists is worth more than an exemption:

test_path_map:
  - source_glob: "**/qaConfig/PHPStan/Rules/**"
    test_dir: "apps/app/qaConfig/Tests"
  - source_glob: "src/**"
    test_dir: "tests/Small"
    mirror: true

A nested layout.test_dirs entry is a mirror root already. layout.test_dirs: ["tests/Small", "tests/Large"] makes the gate search tests/Small/<mirror after src/>/<TestName> (and Large) with no test_path_map entry at all; a bare name such as e2e only classifies.

A path can also be exempted entirely via that handler's exclude_paths option or the project-wide daemon.exclude_paths — additive gitignore-style globs. Prefer test_path_map: excluding turns the gate OFF for those files.

Allowed through without blocking: vendor dirs, node_modules, build outputs, generated files, and file extensions not in the supported language list.

IDs: R-TDD-TEST-FIRST

<!-- handler: error-hiding-blocker -->

error_hiding_blocker — error-suppression patterns are blocked

A Write/Edit of code that silently swallows errors is blocked. All errors must be handled explicitly.

Blocked patterns (examples):

  • Python: bare except clauses with an empty body, catching and discarding all exceptions, and suppress(...) naming Exception or BaseException (also in a tuple); a narrow suppress(OSError) is allowed
  • Shell: redirecting stderr to /dev/null to silence failures, || true to suppress non-zero exit codes
  • JavaScript/TypeScript: empty catch blocks that swallow exceptions
  • Go: if err != nil {} (empty error check, also when the body is only a comment), and a blank in the LAST position of an assignment from a FUNCTION CALL — n, _ := strconv.Atoi(s) — which is where Go returns the error. Not blocked: _, err :=, comma-ok forms (v, _ := m[k], x, _ := y.(T)), for i, _ := range, the explicit _, _ = f() discard, and anything inside a comment or string literal.

Required action: Handle errors explicitly — log them, return them to the caller, or propagate them. Silent error suppression masks bugs and makes debugging impossible.

Excluded paths: vendor/, node_modules/, and test-fixture dirs (tests/fixtures/, tests/assets/, fixtures/) are skipped by default. Exempt more paths with glob patterns via handlers.pre_tool_use.error_hiding_blocker.options.exclude_paths or the project-wide daemon.exclude_paths — use these for fixtures of deliberately-broken code instead of disabling the handler.

IDs: R-ERROR-HIDING

<!-- handler: block-comment-changelog -->

comment_changelog — no changelog narrative in code comments

A Write/Edit that puts HISTORICAL NARRATIVE into a code comment is blocked. A comment describes CURRENT STATE; changelog narrative belongs in git (the commit message), the project's changelog file, or a plan journal entry (mkplan.bash --journal).

Blocked (high-precision) signals, either of which denies the write:

  • Prior <version>: / Previously <version>: phrasing
  • a dated entry (2026-08-12: ...)

Both were measured with ZERO false positives across this project's own ~1,080 source/test files (Plan 00208's whole-repo self-scan) — every real hit was either the field-report shape itself or this handler's own test fixtures.

NOT blocked — advisory only: a version-transition arrow (1.2 -> 1.3), a changelog verb naming a version (Removed in v2.1.224), two or more distinct versioned/dated entries in one comment (configurable via max_history_entries, default 1), Fixed:/Added:/Changed: bullet runs, retrospective phrasing (used to, no longer, we switched from). These four started as blocking signals but the same self-scan found each firing on legitimate code — version-processing utilities (upgrade compatibility checkers) legitimately cite multiple versions in their own docstrings, and "removed in vX.Y" describing an EXTERNAL tool's own deprecation is rationale, not a changelog entry about this project.

History as RATIONALE is legitimate and is NOT flagged. A comment may recount the past when the past is the reason the code looks the way it is now, and re-litigating it would reintroduce a fixed bug — e.g. # Plan 00047: do NOT re-add DISABLE_MOUSE, see.... The separating test: an entry keyed by a RELEASE NUMBER is a changelog; an entry keyed by a FAILURE MODE (a plan number, a bug description) is a rationale.

No escape hatch — unlike comment_size, this handler has no MUST_..._BECAUSE override: changelog content should be MOVED to git/a changelog file/a plan JOURNAL/, never exempted in place.

Scope: only comment spans are scanned (not code), via the same Strategy Pattern language registry as qa_suppression. .md files are skipped entirely — markdown prose is not a comment. Only the ADDED text is checked on Edit (new_string) — removing changelog content is never blocked.

Excluded paths: vendor/build/fixture dirs are skipped by default. Exempt more paths via handlers.pre_tool_use.comment_changelog.options.exclude_paths or the project-wide daemon.exclude_paths.

IDs: R-COMMENT-CHANGELOG

<!-- handler: auto-continue-stop -->

Stop Explanation Required

Before stopping, prefix your final message with STOPPING BECAUSE: followed by a clear reason:

STOPPING BECAUSE: all tasks complete, QA passes, daemon restart verified.

Why: The stop hook enforces intentional stops. Stopping without an explanation triggers an auto-block that asks you to explain or continue.

Alternatives:

  • STOPPING BECAUSE: <reason> — stops cleanly with explanation
  • Continue working — no need to stop unless all work is genuinely complete

Do NOT:

  • Stop mid-task without explanation
  • Ask confirmation questions and then stop (the hook auto-continues those)
  • Smuggle a rhetorical continue question inside a STOPPING BECAUSE: message ('STOPPING BECAUSE: slice 1 done. Want me to build slice 2?') — this is HARD-BLOCKED; the prefix does not exempt tautological questions. Just continue with the next unit of work
  • Use AUTO-CONTINUE unless you intend to keep working indefinitely

Before asking a question, evaluate it critically:

  • Tautological/rhetorical questions with obvious answers ("Should I continue?", "Would you like me to proceed?") — do NOT ask, just do it
  • Errors with a clear next step ("The test failed, should I fix it?") — do NOT ask, just fix it
  • Genuine choice questions where all options are valid ("Which of A, B, or C should we use?") — these deserve a response. Use STOPPING BECAUSE: need user input and ask your question

Recovering from a tool_use_error — do NOT stop silently:

Some tool errors require an explicit recovery action, not a halt. The most common shape:

  • You call Edit or Write on a file you have not yet read.
  • Claude Code returns a tool_use_error (e.g. "File has not been read yet").
  • The correct recovery is Read the file, then retry Edit/Write — do not stop. Stopping silently after a tool error triggers a Stop-hook re-entry loop and wastes a turn.

Rule: Read before Edit/Write. If you must edit a file you have not read, Read it first in the same turn. The daemon's Stop handler will detect a tool_use_error followed by a silent stop and re-fire to force recovery.

On Stop hook re-entry (the hook fires again after a prior block): your next response is treated like any other — it must either prefix with STOPPING BECAUSE: or continue the work. Re-entry does not exempt you from the explanation rule.

If you are stopping because you are blocked ONLY on the human, declare it — it turns the failsafe cron and every declared persistent_crons job off. Put the token immediately after the prefix:

STOPPING BECAUSE: [awaiting-human] the owner has to choose between A and B before anything else can move.

The token is matched exactly, so any wording after it works. These older phrasings are also still recognised, and are kept working rather than extended — a new wording is handled by the token, not by adding another phrase here:

  • blocked only on human input
  • blocked only on the owner's input
  • need user input
  • waiting only on the user's decision

Either form in your STOPPING BECAUSE: line records a marker that makes the daemon drop the next hourly failsafe-cron tick, and every declared job's tick, before it reaches you, at zero token cost. Another cron's tick never clears it, provided its prompt still carries the daemon's [tick:...] first line. Without one, every tick costs a full turn to read and answer with nothing. The next real user message clears the marker and hourly ticks resume; it also expires on its own, so a mistake here costs you at most a day of ticks. The marker is the awaiting-human session mode, ended only by a human prompt in THIS session; every mode and what it suppresses: CLAUDE/SessionModes.md.

Only when it is the ONLY thing blocking you. A stop that merely mentions waiting on someone while other work remains must not use these shapes — that would silence a tick you could have used. If there is work you could still do, do it instead of stopping.

An [awaiting-human] stop must also schedule a one-off stand-in. With persistent_crons.enabled, a main-thread stop that declares it is blocked on the human is denied until session_crons holds a cron led by [tick:stand-in]; the deny gives the exact CronCreate (recurring: false, about options.stand_in_delay_hours = 3 hours out, valid above 0 and below 24). If the marker is still live when it fires, a model: fable sub-agent picks among the options you laid out and the choice is journalled as the stand-in's ruling, never the owner's. It may choose only engineering options: releases, force and remote-branch deletes, QA suppressions, history rewrites, upgrade approvals and anything a guard says to ask the user stay blocked. The stand-in tick is not dropped by the live marker, and a sub-agent or teammate stop is never asked for one.

IDs: R-STOP-QA-FAILURE, R-STOP-TAUTOLOGICAL-QUESTION, R-STOP-AFTER-TOOL-ERROR, R-STOP-CONFIRMATION-QUESTION, R-STOP-NO-REASON, R-STOP-GOAL-LEDGER, R-STOP-STAND-IN-MISSING

All other enforced rules

<!-- handler: auto-approve-reads --><!-- handler: bash-safe-mode --><!-- handler: block-ancestry-severing-merge --><!-- handler: block-artefact-publishing --><!-- handler: block-ask-user-question --><!-- handler: block-comment-size --><!-- handler: block-curl-pipe-shell --><!-- handler: block-dangerous-permissions --><!-- handler: block-git-message-backtick --><!-- handler: block-git-stash --><!-- handler: block-pip-break-system --><!-- handler: block-plan-time-estimates --><!-- handler: block-secret-file-read --><!-- handler: block-self-matching-process-probe --><!-- handler: block-sudo-pip --><!-- handler: block-unread-overwrite --><!-- handler: branch-creation-guard --><!-- handler: branch-stop-guard --><!-- handler: branch-subagent-stop-guard --><!-- handler: conflict-marker-commit-gate --><!-- handler: cron-stop-enforcer --><!-- handler: cron-subagent-stop-enforcer --><!-- handler: daemon-location-guard --><!-- handler: dispatch-declaration --><!-- handler: docs-qa-commit-gate --><!-- handler: docs-qa-edit --><!-- handler: enforce-llm-qa --><!-- handler: enforce-markdown-organization --><!-- handler: enforce-npm-commands --><!-- handler: enforce-project-containment --><!-- handler: failsafe-cron-blockage-suppressor --><!-- handler: flaggable-content-channel-guard --><!-- handler: github-issue-assignment-guard --><!-- handler: github_auto_close_keywords --><!-- handler: host-command-guard --><!-- handler: idle-workable-issue-stop-gate --><!-- handler: issue-filing-gate --><!-- handler: lint-on-edit --><!-- handler: lock-file-edit-blocker --><!-- handler: lsp-noise-checker --><!-- handler: orchestrator-simulate --><!-- handler: plan-done-requires-holding-area --><!-- handler: plan-journal-guard --><!-- handler: plan-qa-commit-gate --><!-- handler: plan-qa-edit --><!-- handler: prevent-destructive-git --><!-- handler: prevent-worktree-file-copying --><!-- handler: quarantine-artefact-read-guard --><!-- handler: reference-repo-freshness --><!-- handler: release-blocker --><!-- handler: release-merge-freeze --><!-- handler: remote-docs-commit-gate --><!-- handler: remote-docs-provenance --><!-- handler: remote-docs-routing --><!-- handler: require-absolute-paths --><!-- handler: require-gh-issue-comments --><!-- handler: require-gh-pr-comments --><!-- handler: resume-guard --><!-- handler: root-recursion-guard --><!-- handler: ruff-format-blocker --><!-- handler: staged-lint-gate --><!-- handler: subagent-context-budget --><!-- handler: subagent-cron-delete-blocker --><!-- handler: subagent-full-qa-blocker --><!-- handler: subagent-handoff-gate --><!-- handler: subagent-report-path-verifier --><!-- handler: subagent-report-size-blocker --><!-- handler: subagent-worktree-write-guard --><!-- handler: usage-pause-gate --><!-- handler: usage-pause-stop-gate --><!-- handler: usage-pause-tool-gate --><!-- handler: validate-eslint-on-write --><!-- handler: validate-instruction-content --><!-- handler: verification-result-gate --><!-- handler: write-protected-paths -->
IDBlockedWhyFix
R-PERMISSION-REQUEST-NON-READ-TOOLa permission request for a tool that is not Read, Glob or GrepOnly read-only tools are auto-approved; a write or execute request is not this handler's to grantControl write/execute operations with PreToolUse hooks
R-BASH-SAFE-MODE-PRELUDE-MISSINGa sequenced Bash invocation with no set safety preludeErrors in earlier statements can be silently ignoredGate with && or `
R-GIT-MERGE-SQUASHgit merge --squashSevers ancestry -- git branch -d refuses the branch foreverUse git merge --no-ff instead
R-GH-PR-MERGE-SQUASHgh pr merge --squashSevers ancestry -- git branch -d refuses the branch foreverUse gh pr merge --merge instead
R-GH-PR-MERGE-REBASEgh pr merge --rebaseSevers ancestry -- git branch -d refuses the branch foreverUse gh pr merge --merge instead
R-ARTIFACT-PUBLISHpublishing an artefact via the Artifact toolThe page lives OUTSIDE the project and the repository cannot audit or retract itWrite the file locally and tell the user its path, or ask a human to publish
R-ASK-USER-QUESTION-UNJUSTIFIEDAskUserQuestion while running unattendedNobody is reading this session, so a question waits for an answer that never comesChoose the option you would have recommended, state the assumption in your output text, and continue
R-COMMENT-SIZEa comment growing past its configured size limitComments should describe current state, not accumulateShorten the comment, or declare MUST_EXCEED_COMMENT_SIZE_BECAUSE
R-CURL-PIPE-SHELL`curlwget ...bash
R-CHMOD-WORLD-WRITABLEchmod 777/chmod a+w/chmod o+wAllows anyone to read, write, and execute, bypassing all file permission securityUse least-privilege permissions instead (755/644/600)
R-GIT-MESSAGE-BACKTICKan unescaped backtick in a double-quoted git commit/tag messageBash performs command substitution inside double quotes -- the span is EXECUTED, not quotedUse single quotes, or git commit -F <file>
R-GIT-STASH-PUSHgit stash / git stash push / git stash saveStashes get forgotten, lost, and block git pullUse git commit instead — WIP commits are fine
R-PIP-BREAK-SYSTEM-PACKAGESpip install --break-system-packagesBypasses PEP 668 protection and can corrupt the system Python installationUse a virtual environment or pip install --user instead
R-PLAN-TIME-ESTIMATETime estimates not allowed in plan documentsTime estimates in plans create false expectations and pressureBreak work into concrete tasks and implementation steps; let the user decide scheduling
R-SECRET-READRead/Write/Edit/NotebookEdit/Grep targeting a protected pathThe file's contents must NEVER be read into context by any route — not Read, not Bash, not an interpreter one-liner, not a copyUse bin/hooks-daemon secret-meta <path> for metadata, or ask the user
R-SECRET-BASH-MENTIONa Bash command whose text mentions a protected pathThe file's contents must NEVER be read into context by any route — not Read, not Bash, not an interpreter one-liner, not a copyUse bin/hooks-daemon secret-meta <path> for metadata, or ask the user
R-SECRET-SCRIPT-AUTHORa script authored via Write/Edit whose content references a protected pathThe file's contents must NEVER be read into context by any route — not Read, not Bash, not an interpreter one-liner, not a copyUse bin/hooks-daemon secret-meta <path> for metadata, or ask the user
R-PGREP-SELF-MATCHa pgrep -f/pkill -f/ps-piped-to-grep probe whose literal pattern matches this command's own argvThe probe always finds itself, so a wait never ends and a check always liesBracket the first character (pgrep -f '[p]rovision.bash'), or wait on a log marker
R-WAIT-ON-WRAPPER-PIDa wait on $! when the backgrounded command starts with a wrapper that forks — denied for setsid, advisory for nohup sh -c, timeout and env sh -c$! is the wrapper's pid, and setsid's parent exits at once, so the wait ends immediately and reports successLet the job write its own pidfile, resolve the child with pgrep -P, or wait on a log marker
R-UNBOUNDED-LIVENESS-LOOPa while/until wait on a process with a sleep-only body and no caprun_in_background has a time limit only in unattended sessions, so a wrong probe waits for everWrap it in timeout 3600 bash -c '…', add a counter, or wait on a log marker
R-PGREP-UNRESOLVED-PATTERNa process probe whose pattern is built by expansion, inside a wait or a killIf it expands to text in this command's argv, the probe counts the callerBracket the pattern where it is built, or wait on a log marker
R-SUDO-PIP-INSTALLsudo pip installConflicts with the OS package manager and can corrupt system PythonUse a virtual environment or pip install --user instead
R-WRITE-CLOBBERWrite to an existing file you have not read this sessionYou cannot know what you are destroying, so you could not report the loss even afterwardsRead the file then retry, or use Edit for a targeted change
R-BRANCH-WIP-CAPcreating a branch while the open work branches are at branches.wip.capWork that is started and never finished piles up as branches nobody returns to; the cap makes an agent finish before it startsMerge, park or delete one of the listed branches, or continue in one of them
R-BRANCH-DORMANTcreating a branch while an unparked, unmerged branch is at the branches.dormancy block stageA branch nobody has touched for weeks is work that is being forgotten; new work waits until each one is finished, parked or deletedMerge it, hooks-daemon branch-park <name> --reason ..., or delete-branch it
R-BRANCH-PARKED-POOLcreating a branch while the parked pool is at branches.parked.cap or holds a branch parked past branches.parked.max_age_daysParking is a pause, not a place to leave work for ever; an un-cleared park is dormant work with a reason attachedMerge it, branch-park <name> --clear, delete it, or re-park it with a NEW reason (a same-reason re-park is refused)
R-BRANCH-MERGED-POOLcreating a branch while the merged pool is at branches.merged.cap or holds an unkept merged branchA merged branch is finished work: it is deleted, or kept on purpose with a reason, so merged branches never pile up unexplainedRun the branch-sweep --clean line below, or keep a branch with branch-keep <name> --reason ...
R-BRANCH-STOP-GUARDa stop that leaves a dirty worktree, unpushed commits or a merged branch behindWork left on a branch nobody returns to is lost work; the guard makes the stopping session finish, park or hand over each branch it touchedRun the commands the message lists for each branch, then stop again
R-BRANCH-SUBAGENT-STOP-GUARDa stop that leaves a dirty worktree, unpushed commits or a merged branch behindWork left on a branch nobody returns to is lost work; the guard makes the stopping sub-agent finish, park or hand over each branch it touchedRun the commands the message lists for each branch, then stop again
R-CONFLICT-MARKER-COMMITa commit whose added lines carry a merge-conflict markerA leftover marker becomes history, and one disguised by the markdown formatter reads as prose to every later checkResolve the conflict at each line listed below, re-stage, and commit again
R-CONFLICT-MARKER-SCAN-TIMED-OUTa commit git could not finish reading for conflict markers within its time limitAn unchecked commit is not a clean one, so it is denied; but no conflict marker was found, and the content is not at faultRetry the same commit; the limit is hit when the host is loaded
R-CRON-STOP-DECLAREDa Stop while a declared persistent_crons job is missing from session_crons or due a refreshA declared cron that does not exist (or that expires within the day) fires no more ticks, so nothing would bring the session backCronCreate (recurring) the named job with the schedule and prompt given, deleting the old copy first when refreshing; or hooks-daemon cron-pause <job>
R-CRON-SUBAGENT-STOP-DECLAREDa SubagentStop while a declared persistent_crons job is missing from session_crons or due a refreshA declared cron that does not exist (or that expires within the day) fires no more ticks, so nothing would bring the session backCronCreate (recurring) the named job with the schedule and prompt given, deleting the old copy first when refreshing; or hooks-daemon cron-pause <job>
R-DAEMON-DIR-CDcd/pushd into .claude/hooks-daemon/Daemon CLI commands must be run from PROJECT ROOT, causing path confusion otherwiseRun daemon commands from project root, e.g. bin/hooks-daemon status
R-DISPATCH-DECLARATION-MISSINGa Task dispatch prompt that declares no plan folder and no non-plan destination (strict mode)A subagent's long-form reply travels over a bounded channel that silently elides an oversized inline report, so its output must go to a declared fileName the plan folder the dispatch belongs to, or say 'not plan work' and declare where its files go
R-DOCS-QA-COMMITa git commit violates a block-level docs QA staged-tree checkMost doc rot that matters at commit time is cross-file drift a single-file edit hook cannot seeFix the content per each finding's remediation below and amend the commit
R-DOCS-QA-EDITa documentation Write/Edit violates a block-level docs QA checkA finding only denies the write when it is BLOCK severity AND the resolved mode for that check is blockFix the content per each finding's remediation below and retry
R-ENFORCE-LLM-QArunning run_all.sh directlyIt prints 200+ lines of verbose output; this project's QA output is the LLM-optimised wrapper's ~16 linesUse the project's LLM-optimised QA wrapper (a sub-agent runs its targeted mode and hands the commit to the coordinator)
R-MARKDOWN-WRONG-LOCATIONMARKDOWN FILE IN WRONG LOCATION — a new .md file written to an unrecognised locationMarkdown files must follow project organization rulesMove it into an allowed location, declare its sub-project under projects:, or configure extra_allowed_markdown_paths
R-MARKDOWN-UNTRACKED-MEMORYUNTRACKED CLAUDE MEMORY IS DISABLED FOR THIS PROJECT — a write to ~/.claude/projects/*/memory/*.mdThat knowledge is per-checkout, un-reviewed, and invisible to teammates — it drifts from the repo and bypasses code reviewDocument it in tracked project docs instead (CLAUDE.md, .claude/rules/*.md, docs/)
R-MARKDOWN-PLAN-SYNCa .claude/settings.json plansDirectory out of sync with the daemon's plan_workflow configPlan workflow requires plansDirectory to match daemon config to redirect writes correctlyFix .claude/settings.json's plansDirectory key, then restart your session
R-NPM-PIPED-COMMANDa piped npm run/npx commandPiping npm/npx commands is pointless — llm: cache files hold the full dataRun the plain command, then query the cache file with jq
R-NPM-NON-LLM-COMMANDa raw npm run/npx command when llm: wrappers existllm: commands provide LLM-friendly, machine-readable outputUse the project's npm run llm:* equivalent instead
R-WRITE-OUTSIDE-PROJECT-ROOTa write whose target is outside the repository rootOutside the repo nothing is version-controlled, reviewed or durable — a container's temp directory is wiped on restart, and every other path rule is scoped to the repo so none of them judges itWrite it inside the repository — untracked/scratch/ is the scratch location
R-FAILSAFE-CRON-SUPPRESSEDA delivered failsafe-cron tick, while the session is in the awaiting-human modeEvery tick against a session blocked only on human input is a guaranteed no-op model turnNothing to do -- this is expected. Send a real message to end the mode and resume ticks
R-FAILSAFE-CRON-BACKED-OFFA delivered failsafe-cron tick, while this session is producing nothing and owes no ledgered workAn hourly tick against a session with nothing to recover costs a full model turn and finds nothingNothing to do -- ticks continue, just less often. Any real user message restores hourly cadence
R-DECLARED-CRON-SUPPRESSEDA delivered persistent_crons tick, while the session is in the awaiting-human, goal-blocked or focus modeA declared job's tick against a session blocked only on human input, or focused on one task, costs a full model turnNothing to do -- this is expected. Send a real message (or hooks-daemon focus off) to end the mode and resume ticks
R-FLAGGABLE-CONTENT-CHANNELa content-revealing git/grep command shape over a flaggable pathIt would reveal flaggable content inside routine command output, with no deliberate Read at allDelegate the WHOLE review to the quarantine subagent instead
R-GH-ISSUE-UNASSIGNEDwork tied to a GitHub issue that is assigned to nobodyUnclaimed issue work can collide with someone else picking the same issue upRun bin/hooks-daemon issue-validity N --claim, then retry
R-GH-ISSUE-ASSIGNED-ELSEWHEREwork tied to a GitHub issue assigned only to someone elseThe issue belongs to another account, and working it would duplicate or collideDo not work on it and do not claim it; tell the user
R-GH-ISSUE-AUTHOR-NOT-APPROVEDwork tied to a GitHub issue opened by an author nobody approvedAn issue is untrusted input; only approved identities may hand work to an agentDo not work on it; tell the user
R-GH-AUTO-CLOSE-KEYWORDa GitHub closing keyword + issue reference in a git/gh messageAuto-closes the referenced issue/PR the moment the commit reaches the default branch, and cannot be disabled repository-sideUse a non-closing reference instead, e.g. Addresses #123
R-DOCKER-ROOT-MOUNTdocker run -v /:/host … (the host root mounted into a container)The container then reads and writes the whole host, past every path-scoped guardMount the specific directory the container needs, e.g. -v "$PWD":/app
R-GH-AUTH-TOKENgh auth tokenPrints the GitHub OAuth token into the transcript, where it is stored and replayedUse gh auth status to check the login; let gh itself authenticate its calls
R-PIP-NON-PYPI-INDEXpip install --index-url <url> / -i <url> / --extra-index-url <url> naming a non-PyPI indexThe index decides which code gets installed and run, and this one is not PyPIInstall from PyPI, or stop and ask the human to run it themselves
R-CRONTAB-REMOVEcrontab -rDeletes every scheduled job with no confirmation and no undocrontab -l to inspect, crontab -e to edit; stop and ask the human to run -r
R-STOP-IDLE-WITH-WORKABLE-ISSUEa main-thread stop with workable issues listed, nothing in flight, and the next one left for a cron tickThe declared cron is a safety net, not a scheduler: a stop that waits for the tick while workable issues exist idles the loop for up to an hourStart the named issue now; or stop naming the real blocker ([awaiting-human] if only a human can unblock it)
R-UPSTREAM-ISSUE-UNVERIFIED-BODYgh issue create against the hooks-daemon tracker with a body no generator producedthat tracker is PUBLIC and an issue cannot be retracted -- a pasted config, log excerpt or absolute path costs the CLIENT permanently, while an over-redacted report costs one round tripGenerate the body with hooks-daemon issue-report and file the file it writes
R-LINT-FAILUREa written/authored file that fails its language's lint checkThe write has already landed on disk; this is a failure report, not a rollbackFix the reported problems with Edit — do not re-Write the file from scratch
R-LOCK-FILE-EDITDirect Write/Edit of a package manager lock fileLock files are generated artifacts; manual edits create checksum mismatches and broken dependency graphsUse the package manager commands instead (e.g. npm install, cargo update)
R-LSP-CONFIG-EXCLUDEa language server config with no exclude for a tree that is not project codeThe language server reports other checkouts' and fixtures' defects against this one, and a noisy stream is skimmedAdd the listed exclude entries (the advisory prints them ready to use); a finding that instead names an entry as harmful means REMOVE it - follow the exact instruction printed, never assume every finding means add
R-LSP-SERVER-STALEa running language server older than the config file its check is anchored toIt is still analysing the scope the OLD config declaredEnd the named process (the harness respawns it on the next LSP use)
R-ORCHESTRATOR-MAIN-THREAD-WRITEWrite/Edit/NotebookEdit from the MAIN THREAD (only when orchestrator blocking is armed; otherwise recorded, never denied)Implementation work belongs in a sub-agent; 73% of this repository's main-thread edits were implementation, which is context the lead should never have spentDispatch the work with the Task tool and let the sub-agent write
R-PLAN-DONE-HOLDING-AREAflipping a plan to Complete when its Success Criteria never mention the pending-release holding areaA plan is done when it is merged AND its release-bound consequences are recorded in the holding area; otherwise they are lost at the next releaseAdd a Success Criteria line citing the holding-area artefact, or stating that the plan has no release-bound consequences and why
R-JOURNAL-HAND-WRITTEN-ENTRYa plan journal entry written by hand, or existing journal content changed (Edit/Write/Bash into a JOURNAL/ day-file or entry file)Only mkplan.bash --journal stamps the real UTC time; hand-typed stamps have landed 40 minutes in the futureWrite the entry body to a fresh file under untracked/scratch/, then run mkplan.bash --journal <plan> <category> <body-file>
R-PLAN-QA-COMMITa git commit violates a block-level plan QA cross-file invariantMost plan rot is cross-file and a single-file edit hook cannot see itAmend the commit to also stage what each finding's remediation names below
R-PLAN-QA-EDITa PLAN.md/README.md Write/Edit violates a block-level plan QA checkPlan QA linting catches issues you can fix immediately, before they reach commitFix the content per each finding's remediation below and retry
R-GIT-RESET-HARDgit reset --hardPermanently destroys all uncommitted changesAsk the user to run it manually
R-GIT-CLEAN-FORCEgit clean -fPermanently deletes untracked filesAsk the user to run it manually
R-GIT-CHECKOUT-DISCARDgit checkout -- <file> / git checkout .Discards local changes to file(s) permanentlyAsk the user to run it manually
R-GIT-RESTOREgit restore <file>Discards local changes to files permanently (--staged/-S is allowed)Ask the user to run it manually
R-GIT-STASH-DROPgit stash dropPermanently destroys a stashed changeAsk the user to run it manually
R-GIT-STASH-CLEARgit stash clearPermanently destroys all stashed changesAsk the user to run it manually
R-GIT-PUSH-FORCEgit push --force / git push <remote> +<refspec>Can overwrite remote history and destroy team members' workAsk the user to run it manually, or coordinate and use --force-with-lease
R-GIT-BRANCH-FORCE-DELETEgit branch -D / git update-ref -d refs/heads/<name> of a branch no remote holdsForce-deletes a branch without a merge check; a tip on no remote loses its commitsPush the branch (git push -u origin <name>) and retry; a branch a remote holds is deleted freely
R-GIT-COMMIT-AMENDgit commit --amendRewrites the previous commit, creating messy history and potential data lossCreate a new commit instead
R-GIT-CHECKOUT-FORCEgit checkout -f / git checkout --forceDiscards every uncommitted change in the working tree, naming no fileCommit or stash first; ask the user if the discard is genuinely wanted
R-GIT-SWITCH-FORCEgit switch -f / git switch --discard-changesDiscards every uncommitted change — the switch spelling of checkout -fCommit or stash first; git switch <branch> alone is never blocked
R-GIT-REFLOG-EXPIREgit reflog expire --expire=nowDestroys the reflog, which is the recovery route the other rules assumeUse a real expiry window (--expire=90.days.ago), which is not blocked
R-GIT-GC-PRUNE-NOWgit gc --prune=nowDrops unreachable objects immediately, so reflog-only history is gonePlain git gc and git gc --auto are not blocked; use a prune window
R-GIT-FILTER-HISTORYgit filter-branch / git filter-repoRewrites every commit in the historyAsk the user to run it manually, on a fresh clone with a backup ref
R-GIT-PUSH-DELETE-REMOTEgit push --delete <name> / git push <remote> :<name> of a branch not merged into the default branch, or of a tag; git push --mirror / --prune alwaysDeletes a branch or tag in the shared remote repository, beyond any local recoveryA branch already merged into the default branch is deleted freely; otherwise do not run it, stop and ask the human to run it themselves
R-GIT-WORKTREE-REMOVE-FORCEgit worktree remove [--force] of a worktree holding dirty files, unregenerable ignored content, or a HEAD no surviving ref holds (forced or not)The removal deletes that work with no recoveryCommit, push or deliberately delete each listed path, then remove
R-GIT-EVACUATE-THEN-DESTROYone command that copies a worktree's or branch's work to an untracked file (git diff/format-patch/stash show -p/show by redirect, tee or -o) and then destroys that worktree or branchThe work survives only as an unreviewed patch file outside historyCommit or push the work on the lane's branch, then remove it; or ask a human
R-GIT-DESTRUCTIVE-UNREADABLEa git command that may destroy changes and cannot be read in fullFail closed: a command this handler cannot parse might be any of the rules aboveRewrite it as one plain git command per call, or ask the user to run it manually
R-WORKTREE-FILE-COPYcp/mv/rsync from a worktree into the main repo's code dirsDefeats worktree isolation, bypasses git tracking, and can nuke untracked work in the target directorycd into the worktree, commit, then git merge back
R-QUARANTINE-ARTEFACT-READreading a quarantined *-opus-security-DETAIL* artefact into the coordinatorA DETAIL artefact holds raw flaggable substance meant for a human or another quarantine agent onlyRead the paired *-opus-security-SUMMARY* artefact instead
R-REFERENCE-REPO-STALEa read of a governed reference clone that is behind or off its default branchreasoning from a stale clone produces conclusions indistinguishable from correct ones -- no error, no failing test, just a wrong answerRun the fix: command printed beside the repo, then retry the read
R-REFERENCE-REPO-NOT-VERIFIEDa read of a governed reference clone with no in-date freshness readingnobody has checked this clone, which is a different fact from it being stale -- and treating the two the same either cries wolf or gives false comfortRun hooks-daemon reference-repos to fetch every governed repo and refresh
R-RELEASE-IN-PROGRESSending the session while untracked/release-state.json records an unfinished releaseA part-done release (version bumped, UNRELEASED/ moved, nothing tagged) is its own broken state, and the state file is the human's authorisation to finish itResume from the recorded last_completed_step and finish the release (RELEASING.md); to abort a failed gate, delete the state file and report which gate failed
R-RELEASE-MERGE-FREEZEa git merge on main, a git push to main, or a gh pr merge of any pull request other than the open release pull request, while untracked/release-state.json records oneThe tagged commit must be the commit CI gated; anything else that moves main invalidates the recorded CI runFinish the release (RELEASING.md), or abort it by deleting the state file, then retry
R-REMOTE-DOCS-STAGED-PROVENANCEa commit staging a remote-docs file without valid provenance frontmatterAn unattributed vendored document that reaches history needs a rewrite to remove, and cannot be refreshed, dated or trusted meanwhileCapture with hooks-daemon remote-docs add <url> and re-stage
R-REMOTE-DOCS-PROVENANCEa write into the remote-docs tree without valid provenance frontmatterA vendored document with no recorded source is indistinguishable from something we wrote ourselves, and cannot be refreshed, dated or trustedCapture with hooks-daemon remote-docs add <url> instead of hand-authoring
R-REMOTE-DOCS-VENDORED-COPYa WebFetch of a URL this project already holds a fresh vendored copy ofThe local copy is faster, costs no network round trip, and is the corpus the remote-docs tree exists to buildRead the local path named in the message, or refresh it if you need newer content
R-ABSOLUTE-PATH-REQUIREDRead/Write/Edit file_path requires absolute pathAmbiguous about the current working directory and can target the wrong fileUse an absolute path starting with /
R-GH-ISSUE-VIEW-NO-COMMENTSgh issue view without --commentsIssue comments contain critical context, clarifications and updates not in the issue bodyUse --json with body and comments (--comments alone omits the body)
R-GH-PR-VIEW-NO-COMMENTSgh pr view without --commentsPR comments contain review feedback and discussion context not in the PR bodyUse --json with body and comments (--comments alone omits the body)
R-RESUME-COLD-LARGEa SendMessage that would resume a stopped, cold agent at or above max_resume_tokensA cold resume re-writes the agent's whole history to the cache before it does any workSpawn a fresh agent from the stopped one's report or handoff, or prefix the message RESUME-LARGE: <reason>
R-ROOT-RECURSION-CATASTROPHICgrep -r/find/rg/du/ls -R/... rooted at /, /proc, /sys, /home, /root, ~, $HOMEWalks the entire filesystem and can pin every CPU core for hoursScope the search to the project (e.g. rg -l "pattern" .)
R-RUFF-FORMAT-WRONG-FORMATTERruff format (and ruff format --check)Black is this project's formatter; running Ruff's restyles every file it touches and the next QA run rewrites them back, burying the churn in an unrelated commitUse ./scripts/qa/run_autofix.sh; ruff check and ruff check --fix are fine
R-STAGED-LINT-FAILUREa staged file fails the cheap syntax check at commit timelint_on_edit only ever runs at Write/Edit time, so a git add of pre-existing content skips it entirelyFix the failing file(s) above and re-stage before committing
R-SUBAGENT-CONTEXT-BUDGETa subagent tool call outside the handoff steps at or above the hard context budgetEvery call re-reads the whole context, so an agent this large spends most of its tokens re-readingWrite the handoff file named in the message, commit your work, SendMessage the lead, then stop
R-SUBAGENT-CRON-DELETECronDelete called from inside a subagentA session cron belongs to the coordinator's session, which is the session that loses coverage when it goesReport the cron id and your reasoning to the coordinator and let it decide
R-SUBAGENT-FULL-QAa full-suite QA run inside a sub-agent (a declared full_qa_patterns command)Concurrent full runs across agents exhaust the host, and the coordinator runs the full gate over every ready branch anywayRun targeted QA on what you changed, commit, and hand the commit to the coordinator
R-SUBAGENT-HANDOFF-MISSINGa subagent stop over the hard context budget with no handoff file writtenA coordinator can only continue the work from a handoff; without one the next agent starts blindWrite the handoff file the message names, then stop
R-SUBAGENT-REPORT-PATH-MISSINGa final message that claims to have written a project file that is not on diskA false report path looks right, gets recorded as evidence, and resolves to nothingWrite the file at that exact path, or stop without claiming a path (do not invent an empty report)
R-SUBAGENT-REPORT-TOO-LARGEa subagent's final message over the character thresholdA subagent's reply travels over a bounded channel that silently elides an oversized inline report in the middle, so the coordinator can receive a report that looks complete and is notPut the full report in a file and reply with its path and a short summary under the threshold
R-SUBAGENT-CROSS-WORKTREE-WRITEa subagent's Write/Edit/NotebookEdit into another checkout of its repositoryEdits land uncommitted in a branch nobody on this task owns, and a later commit there carries them under the wrong branch's nameWrite only inside your own worktree; report any cross-branch need to the coordinator
R-USAGE-PAUSE-PROMPTA prompt, cron tick or supervisor message while the session is paused on its usage ceilingThe session stopped taking on work to keep the account under its usage ceiling, and each prompt would cost a full model turnNothing to do -- the session resumes by itself at the window reset; a human can override it by typing ! bin/hooks-daemon usage-pause clear in the session
R-USAGE-PAUSE-STOPa stop while a usage-paused session's crons are not exactly the one resume cronA paused session is woken only by its resume cron, so any other cron would drive it past the usage ceiling and a missing one would never wake itCronList, CronDelete every cron, CronCreate the one resume cron as the pause directive gave it, then stop again
R-USAGE-PAUSE-TOOLany tool except CronCreate, CronDelete, CronList, SendMessage, TaskStop, ToolSearch while the session is paused on its usage ceilingThe session stopped taking on work to keep the account under its usage ceilingReplace the session's crons with the one resume cron, then stop; the session resumes at the window reset
R-ESLINT-ERRORSa written/authored TS/TSX file with reported ESLint errorsThe write has already landed on disk; this is a failure report, not a rollbackFix the reported problems with Edit (npx eslint <file> --fix clears most)
R-ESLINT-TIMEOUTan ESLint run that did not finish within the configured timeoutThis handler DENIES on a timeout — unlike lint_on_edit, which allowsInvestigate why ESLint is slow (config, project size); retry the edit
R-ESLINT-RUN-FAILUREan ESLint invocation that failed to run at allESLint could not be launched (exception raised invoking it)Check the ESLint wrapper/tsx setup, then retry the edit
R-INSTRUCTION-IMPLEMENTATION-LOGimplementation logs (e.g. 'created the file X', 'added the class Y')Instruction files hold permanent instructions, not a log of past editsRemove the log sentence; put implementation history in git or a plan JOURNAL/
R-INSTRUCTION-STATUS-INDICATORstatus indicators (e.g. checkmark + 'Complete', 'Done', 'Success', 'Fixed')A completion emoji records a moment in time, not a permanent factRemove the status marker; instruction files describe the project, not its history
R-INSTRUCTION-TIMESTAMPtimestamps (ISO dates such as 2024-03-15)A dated entry is a log line, and instruction files are not a logRemove the date; if it is genuinely load-bearing, put it in git history
R-INSTRUCTION-LLM-SUMMARYLLM summaries (section headings such as '## Summary', '## Key Points', '## Overview')A summary heading is the shape an LLM's own turn-report takes, not project documentationRemove the heading and fold any durable content into the surrounding instructions
R-INSTRUCTION-TEST-OUTPUTtest output counts (e.g. '42 tests passed', '1 test failed')A test run's result is a point-in-time fact, not a stable instructionRemove the count; CI already reports this on every run
R-INSTRUCTION-FILE-LISTINGchangelog-style file listings (e.g. 'created src/Service/Foo.php')A file path preceded by a past-tense action verb is changelog narrativeRemove the log line; a bare path reference used as documentation stays allowed
R-INSTRUCTION-CHANGE-SUMMARYchange summaries (e.g. 'Added 15 lines', 'Removed 8 lines')A line-count delta describes one diff, not a stable instructionRemove the summary; the diff itself is preserved in git
R-INSTRUCTION-COMPLETION-INDICATORcompletion indicators (e.g. 'ALL DONE!', 'Task complete!', 'Finished task')A completion phrase announces a session's end, not a fact about the projectRemove the phrase; instruction files should never celebrate finishing a task
R-VERIFICATION-RESULT-NOT-CONSUMEDa verifier followed by a mutator with nothing consuming the resultThe verifier can fail and the mutator would still runGate with &&, `
R-WRITE-PROTECTED-PATHa write, move onto, deletion or truncation of a path the project keeps read-onlyThe file is maintained outside the agent (infrastructure-as-code or a human); an agent's change is overwritten or breaks that processDo not change it; ask the human for any change to the file

Advisories and other active handlers

One line each; these fire with their own guidance when relevant. Full text: bin/hooks-daemon explain-handler <name>.

<!-- handler: agent-isolation-advisor -->
  • agent_isolation_advisor — isolate concurrent agents
<!-- handler: agent-terminated-early-failure-detector -->
  • agent_terminated_early_failure_detector — a dead foreground sub-agent is surfaced on PostToolUseFailure
<!-- handler: autonomy-notice -->
  • autonomy_notice — autonomy is off in this environment
<!-- handler: background-process-tracker -->
  • background_process_tracker — backgrounded processes are tracked
<!-- handler: branch-sweep -->
  • branch_sweep — branches that need attention
<!-- handler: branch-touch-recorder -->
  • branch_touch_recorder — who created and who touched a branch
<!-- handler: budget-exhaustion-detector -->
  • budget_exhaustion_detector — hidden agent budgets are surfaced
<!-- handler: ccy-supervisor-integrity -->
  • ccy_supervisor_integrity — keep the ccy supervisor properly set up
<!-- handler: cleanup-after-merge -->
  • cleanup_after_merge — a merged branch is cleaned up after the merge
<!-- handler: command-hints -->
  • command_hints — advisory reminders after specific commands
<!-- handler: daemon-restart-verifier -->
  • daemon_restart_verifier — restart the daemon before committing
<!-- handler: daemon-sync-after-merge -->
  • daemon_sync_after_merge — a pull can leave the daemon stale
<!-- handler: deployed-artefact-drift -->
  • deployed_artefact_drift — a deployed file has moved away from its template
<!-- handler: docs-qa-sweep -->
  • docs_qa_sweep — documentation drift report at session start
<!-- handler: evacuation-recorder -->
  • evacuation_recorder — an evacuated worktree or branch stays protected this turn
<!-- handler: failsafe-cron-session-advisor -->
  • failsafe_cron_session_advisor — the failsafe cron from session start
<!-- handler: flaggable-work-advisor -->
  • flaggable_work_advisor — delegate flaggable work BEFORE reading it
<!-- handler: focus-mode-trigger -->
  • focus_mode_trigger — hooks-daemon focus <task> points one session at one task
<!-- handler: git-hooks-executable-fixer -->
  • git_hooks_executable_fixer — auto-fixes non-executable git hooks
<!-- handler: git-upstream-checker -->
  • git_upstream_checker — additive fetch + pull/cleanup advice on session start
<!-- handler: goal-injection -->
  • goal_injection — plan-start goal signal for the ccy supervisor
<!-- handler: hook-registration-checker -->
  • hook_registration_checker — hooks configuration policy
<!-- handler: idle-housekeeping-advisory -->
  • idle_housekeeping_advisory — report-first idle housekeeping (beta, opt-in)
<!-- handler: markdown-table-formatter -->
  • markdown_table_formatter — markdown tables are auto-aligned
<!-- handler: merge-qa-advisor -->
  • merge_qa_advisor — merge a work branch with its targeted QA recorded
<!-- handler: merge-qa-report -->
  • merge_qa_report — post-hoc plan/docs QA report after a merge
<!-- handler: model-downgrade-recorder -->
  • model_downgrade_recorder — the automatic model downgrade is written down
<!-- handler: model-fallback-detector -->
  • model_fallback_detector — silent model substitution is surfaced
<!-- handler: nitpick-dismissive-language -->
  • nitpick.dismissive_language — do not deflect or prematurely halt
<!-- handler: nitpick-hedging-language -->
  • nitpick.hedging_language — the guessing is the defect, not the wording
<!-- handler: operator-signal-notice -->
  • operator_signal_notice — a host reboot or shutdown warning reaches you mid-turn
<!-- handler: persistent-cron-assertor -->
  • persistent_cron_assertor — declared crons are re-established each session
<!-- handler: plan-qa-sweep -->
  • plan_qa_sweep — plan-tree drift report at session start
<!-- handler: plan-status-snapshot -->
  • plan_status_snapshot — pre-write PLAN.md status snapshot
<!-- handler: plan-workflow-asset-checker -->
  • plan_workflow_asset_checker — plan tooling provisioning alert
<!-- handler: plan-workflow-guidance -->
  • plan_workflow — PLAN.md, supporting docs and JOURNAL/ obey DIFFERENT contracts
<!-- handler: project-handler-load-checker -->
  • project_handler_load_checker — project protection degraded alert
<!-- handler: recovery-cron-advisor -->
  • recovery_cron_advisor — failsafe recovery cron lifecycle advisory
<!-- handler: reference-repo-sweep -->
  • reference_repo_sweep — reference clones are made fresh before you read them
<!-- handler: routine-qa-sweep -->
  • routine_qa_sweep — recurring work that has stopped recurring
<!-- handler: secret-file-hygiene-checker -->
  • secret_file_hygiene_checker -- on-disk hygiene for protected paths
<!-- handler: session-actions-directive -->
  • session_actions_directive — the must-do list is delivered as a turn
<!-- handler: standing-authorisations -->
  • standing_authorisations — a project can record a standing request
<!-- handler: subagent-report-persistence -->
  • subagent_report_persistence — every sub-agent reply is saved to a file
<!-- handler: tool-disable-advisor -->
  • tool_disable_advisor — declared never-want tools are checked at session start
<!-- handler: worktree-create -->
  • worktree_create — semantic worktree naming
</hooksdaemon>

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.