Skip to content

Latest commit

 

History

History
646 lines (417 loc) · 35.1 KB

File metadata and controls

646 lines (417 loc) · 35.1 KB

PHP OpenAPI Generator — Developer Context

Stack

  • PHP 8.4 (strict_types everywhere)
  • PHPUnit 11 — use PHP attributes, never docblock annotations
    • #[DataProvider('method')] not @dataProvider
    • #[Large] / #[Medium] / #[Small] not @large etc.
    • #[Group('name')] not @group
  • PHPStan at max level via vendor/bin/qa -t phpstan
  • lts/php-qa-ci for QA pipeline — run with vendor/bin/qa -t <tool>

Test sizes

Keep tests as small as possible. Only use #[Large] when test genuinely requires it (e.g. code generation over large OpenAPI specs). Timeouts enforced by qa tool:

  • Small: 1s (default for unannotated)
  • Medium: 10s
  • Large: 300s

Bash command output

DO NOT pipe Bash commands to head or tail. Instead, redirect full output to a temp file and echo only the exit code. Then use the Read tool to parse/analyse as much as needed without re-running expensive commands.

# WRONG — loses output, triggers pipe blocker, may need re-run
vendor/bin/qa -t phpstan 2>&1 | tail -50

# RIGHT — full output preserved, read what you need later
OUTFILE=$(mktemp) && vendor/bin/qa -t phpstan > "$OUTFILE" 2>&1; echo "EXIT_CODE=$?"
# Then: Read tool on $OUTFILE

Running QA tools

ALL QA tools MUST be run via vendor/bin/qa — never invoke PHPStan, PHPUnit, Rector, or CS Fixer binaries directly. The qa wrapper configures paths, configs, and environment correctly.

vendor/bin/qa -t phpstan           # static analysis (max level)
vendor/bin/qa -t phpunit           # full test suite with timeouts
vendor/bin/qa -t phpstan -p <path> # analyse specific path only
php vendor/bin/phpunit             # direct PHPUnit (no time enforcement, use sparingly)

Do NOT run php vendor/bin/phpstan or vendor/bin/phpstan directly — it will not pick up the correct config.

Namespace

All production code: LongTermSupport\OpenApiGenerator\

Key packages (first-party, editable in vendor via --prefer-source)

  • lts/strict-openapi-validator — validates OpenAPI 3.1.x specs; 3.0.x specs produce markTestIncomplete()
  • lts/php-qa-ci — QA pipeline

Coding standards

See CLAUDE/CodingStandardsAndQa.md for the full reference. Key rules:

  • Always use \Safe\ function variants — Rector enforces this, never revert to non-safe
  • Type guards with LogicException to narrow Safe return types — not @var, not casts, not silent fallbacks
  • No @var except array shapes — use instanceof, is_string(), etc.
  • No assert() — use if (!...) { throw new LogicException(...); }
  • No PHPStan suppressions — fix the code, never suppress errors
  • Strict comparisons (===/!==) everywhere

Fixture snapshots

OpenAPI3 fixture tests (JaneOpenApiResourceTest) run the generator then diff generated/ vs expected/. When generator output changes intentionally, copy generated/expected/ for affected fixtures. All .php-openapi configs use namespace LongTermSupport\OpenApiGenerator\Component\OpenApi3\Tests\Expected.

The entire point of this project is to generate PHPStan-max-level code from OpenAPI specs. The expected/ directories ARE the proof. PHPStan MUST scan expected/ — if expected/ has errors, the GENERATOR is broken and must be fixed. Never exclude expected/ from PHPStan.

What gets scanned by what

Path PHPStan Rector CS Fixer
src/ (source code) Scanned Scanned Scanned
tests/ (test code) Scanned Scanned Scanned
fixtures/*/expected/ (golden snapshots proving generator correctness) Scanned Excluded Excluded
*/Generator/Runtime/data/ (runtime templates) Scanned Excluded Excluded
var/test-generated/ (runtime test output) Not visible Not visible Not visible

Rector and CS Fixer are excluded from expected/ because they would reformat it, breaking the diff comparison with generated/. PHPStan is NOT excluded because expected/ must pass max-level analysis — that's the whole point.

Runtime templates (Generator/Runtime/data/) are copied verbatim into generated code under arbitrary namespaces. They MUST use \-prefixed FQCNs (e.g. \LogicException, \ArrayObject). Rector/CS Fixer would strip the \ prefix, breaking generated code.

php-qa-ci — Branch and PR Conventions

Rule: A PR represents a new feature or a bug fix. It never represents a single plan. Plans represent atomic pieces of work. A feature or bug fix may be composed of zero or more plans.

Allowed branch prefixes for PRs: feature/, bugfix/, chore/, hotfix/. Disallowed for PRs: plan/* — plans land as commits on a feature/bugfix branch.

bin/qa -t branchNamePolicy enforces this. CI runs it as part of bin/qa -t allStatic.

Detailed docs (read in vendor): see vendor/lts/php-qa-ci/CLAUDE/branch-policy.md for full convention, rationale, examples of multi-plan feature branches, and how to extend the allow-list per project via qaConfig/branchNamePolicy.yaml.

php-qa-ci — Defence Before Fix (Net + Filter)

Static analysis is the NET (catches the bug CLASS, permanently); TDD is the FILTER (reproduces the INSTANCE on the production path and proves the fix). Belt and braces. When a rule goes RED, fix by WIRING the contract to a real producer — never by deleting the flagged element to silence the rule. Pure coding-standards issues are static-only (no behaviour to assert); behavioural/contract defects get the full net + filter. Every nullable member is two paths — test BOTH (value-present and null); prefer non-nullable where null is not a valid state.

Detailed docs (read in vendor): vendor/lts/php-qa-ci/CLAUDE/DefenceBeforeFix.md.

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.

absolute_path — always use absolute paths

The Read, Write, and Edit tools require absolute paths. Relative paths are blocked.

  • Correct: /workspace/src/main.py, /workspace/tests/test_utils.py
  • Blocked: src/main.py, ./config.yaml, ../other/file.txt

The working directory is /workspace. Prepend /workspace/ to any relative path before calling these tools.

curl_pipe_shell — never pipe curl/wget to bash/sh

Piping network content directly to a shell is blocked. It executes untrusted remote code without any inspection.

Blocked: curl URL | bash, curl URL | sh, wget URL | bash, curl URL | sudo bash

Safe alternative: download first, inspect, then execute:

curl -o /tmp/script.sh URL
cat /tmp/script.sh          # inspect
bash /tmp/script.sh         # execute if safe

daemon_location_guard — do not cd into .claude/hooks-daemon/

Bash commands that change directory into .claude/hooks-daemon/ (or cd into a daemon-internal subdirectory and then run something) are blocked. The daemon is an upstream dependency that must remain untouched in client repos.

Run daemon CLI from the project root instead — it always works regardless of cwd:

$PYTHON -m claude_code_hooks_daemon.daemon.cli status
$PYTHON -m claude_code_hooks_daemon.daemon.cli restart
$PYTHON -m claude_code_hooks_daemon.daemon.cli logs

If you need to inspect daemon source for debugging, use Read from the project root with the absolute path — never cd in. Do NOT edit anything inside .claude/hooks-daemon/; changes will be overwritten on the next upgrade.

dangerous_permissions — chmod 777 is blocked

chmod 777 and other world-writable permission commands are blocked. Overly permissive file permissions are a security vulnerability.

Blocked: chmod 777, chmod 666, chmod a+w, chmod o+w

Use least-privilege permissions instead:

  • Executable scripts: chmod 755 (owner rwx, group/other rx)
  • Regular files: chmod 644 (owner rw, group/other r)
  • Private files: chmod 600 (owner rw only)

destructive_git — blocked git commands

The following git commands are permanently blocked and will always be denied:

Command Reason
git reset --hard Permanently destroys all uncommitted changes
git clean -f Permanently deletes untracked files
git checkout -- <file> Discards all local changes to that file
git restore <file> Discards local changes (--staged is allowed)
git stash drop Permanently destroys stashed changes
git stash clear Permanently destroys all stashes
git push --force Can overwrite remote history and destroy teammates' work
git branch -D Force-deletes branch without checking if merged (lowercase -d is safe)
git commit --amend Rewrites the previous commit — create a new commit instead

If the user needs to run one of these, ask them to do it manually. Do not attempt to work around the block.

Safe alternatives: git stash (recoverable), git diff / git status (inspect first), git commit (save changes permanently first).

error_hiding_blocker — error-suppression patterns are blocked

Writing 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
  • 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: _ = err (discarding error return values without handling)

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

gh_issue_comments — always include --comments on gh issue view

gh issue view without --comments is blocked. Issue comments often contain critical context, clarifications, and updates not in the issue body.

Blocked: gh issue view 123, gh issue view 123 --repo owner/repo

Allowed: gh issue view 123 --comments, gh issue view 123 --json title,body,comments

If using --json, include comments in the field list instead of adding --comments.

gh_pr_comments — always include --comments on gh pr view

gh pr view without --comments is blocked. PR comments often contain review feedback, reviewer requests, and decisions not in the PR body.

Blocked: gh pr view 123, gh pr view 123 --repo owner/repo

Allowed: gh pr view 123 --comments, gh pr view 123 --json title,body,comments

If using --json, include comments in the field list instead of adding --comments.

git_stash — git stash is blocked by default

git stash, git stash push, and git stash save are blocked. git stash pop, git stash apply, git stash list, and git stash show are always allowed.

Why: stashes get forgotten, lost, and block git pull. Use git commit -m 'WIP: ...' instead — WIP commits are acceptable.

Escape hatch (when commit truly won't work):

MUST_STASH_BECAUSE="explain why"; git stash

Configure via handlers.pre_tool_use.git_stash.options.mode: warn for advisory-only mode.

lock_file_edit_blocker — never directly edit lock files

Direct Write or Edit to package manager lock files is blocked. Lock files are generated artifacts; manual edits create checksum mismatches and broken dependency graphs.

Blocked files: composer.lock, package-lock.json, yarn.lock, pnpm-lock.yaml, Gemfile.lock, Cargo.lock, go.sum, Package.resolved, Pipfile.lock, and others.

Use package manager commands instead:

  • PHP: composer install / composer require package
  • Node: npm install / yarn add package
  • Ruby: bundle install / bundle add gem
  • Rust: cargo add crate
  • Go: go get module

markdown_organization — tracked-docs policy (untracked Claude memory BLOCKED)

This project sets allow_untracked_claude_memory: false. Writing to Claude auto-memory files (~/.claude/projects/*/memory/*.md) is blocked — via the Write/Edit tools AND via bash redirect/tee side-doors. Reading memory is still allowed so existing memory can be migrated out.

Put durable knowledge in TRACKED project docs (progressive disclosure):

  • Always-relevant facts → CLAUDE.md (keep lean; resident every session)
  • Path-specific guidance → .claude/rules/*.md with paths: glob frontmatter (loads on demand only when matching files are touched)
  • Intent-triggered procedures → a thin skill under .claude/skills/ pointing at a single-source-of-truth doc body
  • Human-facing reference → docs/
  • Link docs with plain markdown links (zero token cost until followed); avoid @-imports (they re-inline eagerly rather than defer)

Keep ONE source of truth per fact and link to it. Normal markdown-location rules (below) still apply to every other .md file.

Allowed locations: CLAUDE/, docs/, RELEASES/, CLAUDE/Plan/, root-level README.md, .claude/rules/, or any extra_allowed_markdown_paths pattern.

pip_break_system — --break-system-packages is blocked

pip install --break-system-packages (and the pip3 / python -m pip / python3 -m pip variants) is blocked. The flag bypasses PEP 668 system-package protection and corrupts the system Python environment in containers and on modern Linux distros.

Use a virtualenv or --user install instead:

python3 -m venv /tmp/venv && /tmp/venv/bin/pip install <package>
# or
pip install --user <package>

If a tool's installer insists on --break-system-packages (some quick-start scripts do), download it first, inspect, and run it inside a venv — do not shortcut by adding the flag.

Pipe Blocker

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

Use a temp file instead:

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

# RIGHT — redirect to temp file:
pytest tests/ > /tmp/pytest_out.txt 2>&1
# Then read selectively if needed

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

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

plan_number_helper — use mkplan.bash to create a plan

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

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

(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 and add 1 — this is the fallback, not the primary path:

git config --local hooksdaemon.latestPlanNumber

Add 1 to that value (zero-pad to 5 digits, e.g. counter 117 → next plan 00118). The git counter is the source of truth; the daemon keeps it correct across branches.

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).

plan_time_estimates — plans describe WHAT, not WHEN

Writing time estimates into a CLAUDE/Plan/*.md file is blocked. Plans capture the work to be done, not how long it will take.

Blocked in plan documents:

  • Effort estimates — **Estimated Effort**: 4 hours, Total Estimated Time: 2 days
  • Per-phase durations — Phase 1: ... (3 days), takes 8-12 hours
  • Target/completion dates — **Target Completion**: 2026-06-30, Completion: 2026-06-30
  • ETA:, timeline:, deadline:, due date: lines

Instead: break work into concrete tasks and implementation steps, and let the user decide scheduling. Technical durations that describe a feature (cache TTL, session timeout, retention window) are allowed — only work/effort estimates are blocked.

qa_suppression — QA suppression annotations are blocked

Writing QA suppression directives into source files 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)
  • 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.

root_recursion_guard — recursive scans rooted at / are blocked

A recursive scanner whose path argument resolves to a catastrophic root location is blocked, because it walks the entire filesystem and can pin every CPU core for hours.

Blocked (recursive scanner + dangerous root path):

  • grep -r/-R/-rl, ugrep -r, rgrep, find, fd/fdfind, rg
  • pointed at /, /proc, /sys, /home, /root, ~, $HOME

Allowed: the same scanners scoped to the project — rg -l "x" /workspace, grep -rl "x" "$CLAUDE_PROJECT_DIR", grep -rl x src/, find . -name y. Non-recursive grep x /etc/hosts is not affected.

Note: ... | head does NOT bound a -l/-rl scan — a producer that matches nothing never writes, so it never receives SIGPIPE and runs to completion across the whole disk.

Escape hatch (rare legitimate whole-disk scan):

MUST_SCAN_ROOT_BECAUSE="explain why"; grep -rl x /

security_antipattern — OWASP security antipatterns are blocked

Writing code that contains security antipatterns is blocked across all supported languages. Fix the code to use safe patterns instead.

Blocked categories:

  • SQL injection: building queries via string concatenation (use parameterised queries)
  • Command injection: passing unvalidated input to subprocess (use argument lists)
  • Hardcoded credentials: API keys, passwords, tokens embedded in source code
  • Weak cryptography: MD5 or SHA1 for password hashing (use bcrypt/argon2)
  • Path traversal: unvalidated user input used in file paths

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

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.

Blocked:

  • sed -i / sed -e (in-place file editing via Bash tool)
  • grep -rl X | xargs sed -i (mass file modification)
  • Shell scripts (.sh/.bash) written via Write tool that contain sed

Allowed (read-only, no file modification):

  • cat file | sed 's/x/y/' | grep z (pipeline transforming stdout only)
  • sed mentioned in commit messages, PR bodies, or .md documentation files

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)

sudo_pip — sudo pip install is blocked

sudo pip install (and the sudo pip3 / sudo python -m pip / sudo python3 -m pip variants) is blocked. Installing as root corrupts the system Python managed by the OS package manager and creates permission/ownership issues that are painful to recover from.

Use a virtualenv or --user install instead:

python3 -m venv /tmp/venv && /tmp/venv/bin/pip install <package>
# or
pip install --user <package>

Even in a container running as root, sudo adds nothing — drop it and use a venv.

tdd_enforcement — test file must exist before source file

Creating a production source file is blocked until a corresponding test file exists.

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
  • Collocated: {source_dir}/{module}.test.ts (JS/TS projects)
  • Test subdirectory: {source_dir}/__tests__/{module}.test.ts

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

validate_instruction_content — CLAUDE.md and README.md must have stable content

Writing ephemeral or session-specific content to CLAUDE.md or README.md is blocked. These files should contain only stable instructions, not implementation logs or session state.

Blocked content types:

  • Timestamps and ISO dates
  • Status emoji followed by completion words (e.g. checkmark + 'Done')
  • Implementation log sentences ('created the file X', 'added the class Y')
  • Test output counts ('3 tests passed')
  • LLM summary section headings ('## Summary', '## Key Points')

Content inside markdown code blocks is exempt from validation.

worktree_file_copy — do not copy files between worktrees and the main repo

cp, mv, and rsync operations that move files from a worktree directory (untracked/worktrees/ or .claude/worktrees/) into the main repo (src/, tests/, config/) — or vice versa — are blocked.

Worktrees are isolated branches. Cross-copying corrupts that isolation and can silently overwrite in-progress work.

Allowed: operations within the same worktree branch. To merge changes: use git merge or git cherry-pick instead.

background_process_tracker — backgrounded processes are tracked

A PostToolUse advisory that fires when a Bash call backgrounds a process (run_in_background: true, or a &/nohup/setsid/disown command). It records the command to background-processes.jsonl and injects rate-limited guidance.

The daemon never kills. It surfaces runaways; you decide.

When you background a long-lived process:

  • Create a non-durable recurring watchdog cron (CronCreate, durable:false) whose prompt runs $PYTHON -m claude_code_hooks_daemon.daemon.cli harvest-background and acts on any runaway — this covers the idle/compaction window a tool-call hook cannot. Do NOT wait for the cron; keep working.
  • Check on demand: run harvest-background (exit 1 == runaways surfaced).
  • Reap a runaway by its process group: kill -- -<pgid> (not just the pid).
  • Keep a wanted long task: note KEEP_RUNNING_BECAUSE="reason".
  • Delete the watchdog cron (CronDelete) when no backgrounded work remains.

Advisory is rate-limited per session (default-on). Disable with handlers.post_tool_use.background_process_tracker.enabled: false.

git_hooks_executable_fixer — auto-fixes non-executable git hooks

When a git command prints hint: The '...' hook was ignored because it's not set as executable, this handler automatically chmod +xs every non-.sample file in the repository's hooks directory (resolved via git rev-parse --git-path hooks, so worktrees and core.hooksPath are handled). Execute bits are added with least privilege (only where read is already granted). It never blocks the command and reports which hooks it fixed via advisory context. .sample files and already-executable hooks are left untouched.

markdown_table_formatter — markdown tables are auto-aligned

After every Write or Edit of a .md or .markdown file, the content is re-formatted via mdformat + mdformat-gfm so that table pipes are aligned and column widths are consistent. The handler is non-terminal and advisory — it never blocks, it just rewrites the file on disk.

What changes:

  • Table pipes are aligned vertically and delimiter rows widened to match cell widths.
  • Ordered lists keep consecutive numbering (1. 2. 3.).
  • --- thematic breaks are preserved (mdformat's 70-underscore default is post-processed back).
  • Asterisks in table cells are escaped (*\*) as required by GFM.

Ad-hoc formatting of existing files:

$PYTHON -m claude_code_hooks_daemon.daemon.cli format-markdown <path>

recovery_cron_advisor — failsafe recovery cron lifecycle advisory

An advisory PostToolUse handler that fires across a plan's lifecycle and injects guidance telling the agent to manage a non-durable hourly failsafe recovery cron.

What it does

Three lifecycle phases are detected from Write/Edit to CLAUDE/Plan/<digits>-<name>/PLAN.md (never from files inside Completed/) and from mkplan.bash Bash invocations:

Phase Trigger Guidance injected
Creation New PLAN.md written, or mkplan.bash invoked Create a non-durable hourly cron now (CronCreate, durable:false); record the ID in the plan; do NOT wait for the cron.
Progress Edit to PLAN.md touching task-status icons (⬜/🔄/✅) or ## Notes & Updates section Confirm the recovery cron is still running (CronList); recreate if missing; keep working.
Completion **Status**: Complete[d] written/edited Plan complete — warns first: deleting now leaves the still-live session with no recovery coverage. Keep the cron if any further work may happen (it is non-durable and dies on session exit); CronDelete only when certain the session is finished.

Progress reminders are rate-limited per plan: the handler advises on the first progress edit and then once every few progress edits for that plan, so it does not spam context on every edit. Completion always advises (bypasses the interval).

CRITICAL: recovery cron is NOT a heartbeat

The recovery cron is a failsafe safety net, not a pacing mechanism:

  • The agent must never wait for the cron between units of work.
  • Work proceeds at full speed until an external factor (Claude API error, rate limit, 5-hour usage limit, network failure) actually stalls it.
  • The cron fires only while the REPL is idle; it cannot interrupt active work.
  • Treating the cron as a heartbeat is an own goal — it would convert a safety net into an artificial hourly throttle.

Canonical recovery-cron prompt

Use this verbatim as the CronCreate prompt:

**FAILSAFE RECOVERY CHECK (automated hourly safety net — NOT a heartbeat).**
If your most recent work on the active plan/task was interrupted by an
*external* factor (Claude API error/overload, rate limit, 5-hour usage limit,
network failure) and is now resumable, resume it immediately and carry it to
completion. If you are blocked **only** on human input, do nothing and keep
waiting. If work is already proceeding normally, this is a **no-op** — do not
interrupt, restart, or duplicate anything in flight. Never treat this as a
heartbeat or pacing signal: between checks, continue at full speed until an
external factor actually stops you — waiting for the cron is an own goal. Do
NOT delete this cron merely because a tick finds nothing to resume: it is
non-durable and ends automatically when the session exits, and a still-live
session stays exposed to the next rate limit without it. Remove it (CronDelete)
only once the session is genuinely finished with no further work.

Configuration

This handler is on by default (opt-out). Disable with:

handlers:
  post_tool_use:
    recovery_cron_advisor:
      enabled: false

hook_registration_checker — hooks configuration policy

On every new session this handler audits hook configuration across .claude/settings.json and .claude/settings.local.json. When it reports issues, fix them — do not ignore the warning.

Policy

  1. All hooks live in settings.json. That file is tracked in version control, visible to teammates, and is the single source of truth for the daemon.
  2. settings.local.json must contain ZERO hooks entries. It exists for per-developer permissions and IDE state only. A hooks block there is either (a) invisible to the rest of the team, or (b) duplicated with settings.json — in which case the hook fires twice per event.
  3. Hook commands must invoke the daemon wrapper. Every registered command must end with /.claude/hooks/{event}. Anything else (inline Python, custom shell scripts, bespoke paths) is a legacy setup that bypasses the daemon entirely.

Remediation

  • Hooks in settings.local.json: move each hooks entry to settings.json, then delete the hooks key from settings.local.json. Confirm no duplicates remain.
  • Legacy-style commands: replace them with a project-level handler. Run $PYTHON -m claude_code_hooks_daemon.daemon.cli init-project-handlers to scaffold .claude/project-handlers/, port the logic into a handler class, then restore the daemon wrapper in settings.json. The daemon will auto-discover the new handler on restart.
  • Missing hooks: the daemon's installer writes the full set. If any are missing, re-run install.py or manually add the missing {event_name} entry pointing at "$CLAUDE_PROJECT_DIR"/.claude/hooks/{bash-key}.
  • Duplicate hooks: a hook registered in both files fires twice. Keep the settings.json entry, delete from settings.local.json.

auto_approve_reads — gated on bypassPermissions mode

Read-only tool permission requests (Read, Glob, Grep) are auto-approved only when Claude Code reports permission_mode == "bypassPermissions" (YOLO mode).

In every other mode (default, plan, acceptEdits, dontAsk) the handler defers and Claude Code's normal approval prompt is shown — the user has not opted out of per-tool approvals, so the daemon must not silently approve on their behalf.

If a permission prompt for Read appears in default mode, that is correct behaviour — approve it via Claude Code's UI.

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)
  • 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/Writedo 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.

dismissive_language_detector — do not deflect or prematurely halt

Stop-time advisory that fires on language patterns signalling avoidance of work. The handler does NOT block the stop, but injects context for the next turn so the agent self-corrects.

Avoid:

  • Dismissing issues as pre-existing, out of scope, not our problem, or not relevant to deflect work that is in fact yours.
  • Premature-halt phrasing like natural checkpoint, ready to continue on your cue, pausing here mid-plan when there is more to do — finish the task rather than dressing up a halt.
  • Speculative should be fine or probably works when verification is cheap (run the test, read the file).

Do: acknowledge the issue, fix it, or — if it genuinely is out of scope — say so once with the specific reason and continue with the in-scope work.