Hook Safety and Robustness Rules

August 8, 2026 · View on GitHub

This document outlines the design standards for Node.js-based terminal hooks, git hooks, and pre/post-tool execution scripts.

1. Exception Isolation & Graceful Fallbacks

  • No Blocking Failures: All hooks must wrap their core execution logic in try-catch blocks. If a hook fails (e.g., due to file system permission issues or missing dependencies), it must SWALLOW the error and exit gracefully with code 0, producing NO output (per Phoenix #13 — Zero Noise; a logged warning would violate it, and the shipped hooks are silent). It must never cause the parent process (git, CLI, or agent) to fail or abort.
  • Graceful degradation: If external binaries (e.g., git, node, or stack-specific compilers) are missing or fail, the script should fallback to reporting mode or a safe default state rather than throwing unhandled exceptions.

2. Execution Latency & Performance

  • Fast Execution: Hooks that run inline during user tasks (e.g., PostToolUse or pre-commit hooks) must execute in under 100ms.
  • Lazy Loading: Avoid importing large packages or executing heavy file-system scans at the top level of scripts. Load resources dynamically only when a scan condition is met.
  • Asynchronous Operations: Perform file writing or analytics logging asynchronously so as not to block the CLI's main thread.

3. Cross-Platform Directory & Shell Handling

  • Path Normalization: Never hardcode directory separators (/ or \). Always use Node's path library.
  • Case-Insensitive Windows Path Checks: Remember that Windows paths are case-insensitive. When comparing paths on win32 platform (e.g., checking if a file is already in a list of touched files), normalize them to lowercase to avoid duplicate detections.
  • Prefer the no-shell exec form over escaping: When a hook spawns a child process with a dynamic path, pass arguments as an ARRAY to a no-shell spawn (Node spawn(cmd, [args]) without shell: true; or a hook's args: [] exec form) — a path containing quotes, $, or backticks then never reaches a shell parser. Per-platform hand-escaping (cmd.exe double-quotes vs PowerShell backticks vs bash single-quotes) is the FALLBACK only when a shell is unavoidable; it is error-prone and a common injection vector.

4. Output Formatting & Verbosity

  • No Log Clutter: Produce NO output during normal operation — the hooks are silent (Phoenix #13). The ONLY sanctioned outputs are the three channels named in commandment #13 (Zero Noise) — the Stop hook's structured JSON block when an action is required, and conductor context injection on SessionStart or UserPromptSubmit; never emit incidental logs, warnings, or status lines.
  • Clear Indicators: When a hook DOES emit on one of those sanctioned channels, use a clean, standard prefix (e.g., [CoalMine]) so the user knows the source.

5. Localization & Adaptive Language

  • Adaptive Language: Hooks that output user-facing warnings or prompt the user must detect the user's language and adapt dynamically.
  • Heuristic-Based Language Detection: If the environment context is not passed directly, detect language by scanning project documentation (e.g., AGENTS.md, MEMORY.md, README.md) for regional characters (e.g., Thai Unicode characters \u0e00-\u0e7f). If detected, display messages in the local language; otherwise, default to English.

6. Phoenix Canary — 13 Commandments

All CoalMine hooks and canary skill scripts must conform to the Phoenix Canary philosophy. A Phoenix Canary is immortal, zero-footprint, and self-sufficient. Each commandment maps to a measurable property:

#Commandment (TH)PrincipleImplementation Requirement
1ไม่ขับถ่ายZero GarbageDelete every temp file on completion or failure. Use finally blocks to guarantee cleanup.
2ไม่กินอาหารZero DependenciesUse only Node.js built-in modules (fs, path, os). No npm install required to run.
3ไม่หายใจZero LatencyPostToolUse hooks must add ≤5ms of work beyond interpreter startup on the happy path (no file match); total wall-clock ≤100ms including a scan. Node startup itself (~50–80ms) dominates — budget the work, not the process.
4ไม่มีทางตายFail-silentWrap all logic in try { main(); } catch {}; never set a non-zero exit code. Let the process exit naturally — do NOT call process.exit(), it can truncate pending stdout writes (the Stop hook's JSON nudge). Never crash the parent agent.
5ไม่สืบพันธุ์Zero Side-effectsNever spawn child processes, write to global config, or trigger other hooks as side effects.
6ไม่มีตัวตนStatelessNo global state between invocations. Session state lives in temp files scoped by session_id, cleaned on stop.
7ไม่พึ่งพาใครOffline-capableNo network calls ever. All lookups must be local filesystem only.
8ไม่กลายพันธุ์DeterministicSame input → same output, always. No random IDs, no time-based branching outside timestamp stamps.
9ไม่จำกัดร่างPortableRuns on Windows, macOS, Linux without modification. Use path.join(), os.homedir(), os.tmpdir().
10ไม่ล้ำเส้นSandbox CompliantNever read or write outside os.tmpdir() (session state) and os.homedir()/.claude/ (mode config) — EXCEPT reading the project config from the project git root: <project>/.<agent-dir>/coal/<skill>.json (own-agent-dir → .claude.agents.gemini, first-found-wins), LEGACY root dotfile (.coalmine.json/.coaltipple.json/.coalboard.json) still read until the flock's next MAJOR. (Writes stay strictly inside the two sandbox roots.)
11ไม่แก่ตัวFuture-proofUse stable Node.js built-ins only. No deprecated APIs. Compatible with Node 22+ (the maintained LTS line the repos' CI tests, 22 · 24; 18/20 are EOL).
12ไม่ต้องการผู้ดูแลSelf-healingOn any unexpected state (corrupt temp file, missing session ID), silently skip and return cleanly.
13ไม่ส่งเสียงZero NoiseHooks output NOTHING to stdout/stderr except the three sanctioned channels: the Stop hook's structured JSON block when an action is required, and conductor context injection on SessionStart or UserPromptSubmit (agent-context stdout — the shipped CB/CT/CW conductors). Everything else is silent.

7. Hermetic Hook Testing

Fail-silent code hides its own breakage — a hook that crashes looks identical to a hook that found nothing. Every behavior change to a hook therefore ships with a hermetic spawn test:

  • Spawn the real hook file as a child process with fixture stdin — never extract its logic into an importable function just to make testing easier.
  • Sandbox the environment: point TEMP/TMP/TMPDIR and USERPROFILE/HOME at a throwaway directory so real session state and kill-switch files can never affect the test.
  • Assert all three observable surfaces: exit code 0 on every path; stdout/stderr silent except the sanctioned channels (Phoenix #13); and the expected state effect (file written, file cleaned, or nothing touched).
  • Zero-dep (node:test only, per scripts-quality.md section 2) and enumerated explicitly in the gate hooks.

Exemplar: husky and lefthook keep isolation test suites despite tiny codebases; CoalMine's own scripts/lib/hooks.test.mjs is the in-repo reference implementation.