Cross-platform gstack enforcement hook
Design decisions
- Required mode generates
.claude/hooks/check-gstack.cjs. The explicit CommonJS extension is unaffected by a consumer repository'spackage.jsonmodule type. - The one registered command launches Node 18+ and resolves
CLAUDE_PROJECT_DIRinside JavaScript throughprocess.env. It contains no Bash, PowerShell, or cmd environment expansion. - The launcher validates
CLAUDE_PROJECT_DIRbefore callingpath.join. Missing or empty project context emits a structuredPreToolUsedenial, writes a clear reason to stderr, and exits zero instead of surfacing a hook execution error. - Path resolution and module loading run inside the same
try/catch. A nonexistent project, an existing project without the generated hook, or any other load failure returns the same concise denial without exposing a stack trace or the full project path. - The generated hook resolves the user home in
HOME,USERPROFILE, thenos.homedir()order so POSIX, Git Bash, PowerShell, and cmd runners use the expected global install root. - The hook contract follows
docs/spikes/claude-code-hook-mutation.md: pass-through writes{}; denial writes ahookSpecificOutputobject forPreToolUsewithpermissionDecision: "deny". - Missing or unreadable gstack is an intentional denial with exit code zero. Install instructions are present in stderr and
permissionDecisionReason, so the runner does not report a hook execution error. - Rerunning required mode replaces a matching legacy
check-gstack.shregistration and removes duplicate matching registrations.
Deviations
None. The implementation uses the requested Node launcher, one hook handler, and the repository's documented hook output schema.
Tradeoffs
- The generated project now requires Node 18+ at hook execution time. This removes shell selection ambiguity and matches the requested runtime floor.
- The generated hook is invoked through Node rather than as an executable file, so POSIX executable mode bits are no longer part of the contract.
Open questions
None.