Close Menu
ToolTechBlogToolTechBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    Lab testing giant Quest Diagnostics confirms patients’ personal and health data was compromised during breach

    October 23, 2026

    A new subscription service promises an evolving collection of ‘bold and bizarre’ indie games

    October 23, 2026

    Oura sues rival Ultrahuman again, threatening its U.S. smart ring comeback

    October 23, 2026
    Facebook X (Twitter) Instagram
    ToolTechBlogToolTechBlog
    • Home
    • AI Tools
    • Web Hosting
    • Tech
    • Digital Marketing
    • Business Software
    • VPN & Cybersecurity
    ToolTechBlogToolTechBlog
    Home»Web Hosting»Build Deterministic Gates and Linting
    Web Hosting

    Build Deterministic Gates and Linting

    Tool Tech TeamBy Tool Tech TeamOctober 19, 2026No Comments17 Mins Read
    Facebook Twitter Pinterest Telegram LinkedIn Tumblr WhatsApp Email
    Build Deterministic Gates and Linting
    Share
    Facebook Twitter LinkedIn Pinterest Telegram Email

    Claude Code Hooks: Enforce Project Conventions with Gates and Linting

    SitePoint Team

    SitePoint TeamPublished inAI·Programming·
    October 18, 2026

    The AI briefing for Developers

    Stay up to date with AI tools, model releases, and developer workflows that matter.

    Weekly. Free. One click to leave.

    SitePoint Premium
    Stay Relevant and Grow Your Career in Tech

    • Premium Results
    • Publish articles on SitePoint
    • Daily curated jobs
    • Learning Paths
    • Discounts to dev tools

    7 Day Free Trial. Cancel Anytime.

    How to Configure Claude Code Hooks to Enforce Project Conventions

    1. Install prerequisites: ensure jq, Prettier, and ESLint are available as local node_modules dependencies in your project.
    2. Create a scripts/ directory in your project root to hold all hook shell scripts.
    3. Writescripts/pre-write-gate.sh to block the first file-write per prompt and print a duplication-check checklist.
    4. Writescripts/reset-gate.sh to delete the gate temp file, re-arming the gate on each new user prompt.
    5. Writescripts/post-edit-lint.sh to run Prettier (auto-fix) then ESLint (check) on every changed JS/TS file after each edit.
    6. Configure.claude/settings.json with PreToolUse, PostToolUse, and UserPromptSubmit entries pointing to each script.
    7. Test each hook manually by piping sample JSON payloads into the scripts and verifying exit codes and output.
    8. Commit the scripts/ directory and .claude/settings.json to version control so every team member gets the same guardrails.

    Claude Code hooks solve a problem that every developer running agentic coding sessions eventually hits: the AI drifts from project conventions, and soft instructions in the prompt cannot prevent it. This article walks through two working hooks — a pre-write gate and a post-edit lint pipeline — that enforce conventions deterministically.

    Table of Contents

    When Prompt Instructions Aren’t Enough

    The Problem with Long-Session Convention Drift

    Claude Code hooks solve a problem that every developer running agentic coding sessions eventually hits: the AI drifts from project conventions, and soft instructions in the prompt cannot prevent it. A popular r/Frontend post captured this precisely. The developer reported that Claude Code wrote roughly 1,000 lines of code for a simple helper function that already existed in a project dependency. The prompt stated the project conventions. Claude ignored them anyway. The result was duplicated logic, bloated output, and wasted review time.

    The root cause is structural, not incidental. Prompt-based instructions are probabilistic. They work well in short sessions, but as the context window fills during extended agentic loops, earlier instructions degrade in influence. Tool outputs, file contents, and intermediate reasoning bury the initial convention reminders. A long enough session means the agent effectively forgets what it was told. Any enforcement mechanism that relies solely on prompt text will exhibit this degradation pattern.

    Hooks as Deterministic Guardrails

    Claude Code hooks offer an alternative enforcement mechanism: shell scripts that bind to lifecycle events inside the agent loop. These scripts use exit codes to enforce rules. Exit 0 means proceed. Exit 2 blocks the action, and the script’s stderr is surfaced to Claude as feedback. Any other non-zero exit code is a non-blocking error. This is deterministic. No matter how full the context window gets, an exit code of 2 stops the action every time.

    No matter how full the context window gets, an exit code of 2 stops the action every time.

    Prerequisites: Claude Code installed and functional; a Node.js project with Prettier and ESLint already configured; jq installed (brew install jq / apt install jq); Prettier and ESLint must be installed as local node_modules dependencies (not relied upon; and basic familiarity with Bash scripting

    How Claude Code Hooks Work

    Hook Lifecycle Events

    Hooks operate on an event-driven model. They fire at specific points in Claude Code’s agent loop, intercepting actions before or after they occur. Four lifecycle events are relevant:

    PreToolUse fires before any tool call executes. This is the interception point for blocking actions. For file-write gating, the tool names to match are Write, Edit, and MultiEdit. You can also match against Bash for command-level gating.

    After a tool call completes, PostToolUse runs. This is the point for validation: linters, formatters, or test suites check the result of an edit. Because the tool has already executed, an exit code of 2 from a PostToolUse hook does not block the original action. Instead, it surfaces the script’s stderr as feedback to Claude, which can then issue a follow-up edit to correct the issues.

    When the user submits a new prompt, UserPromptSubmit fires. This is useful for resetting state between conversational turns.

    Notification fires when Claude sends a notification. Non-zero exit codes from Notification hooks do not block any action. (The documentation does not specify any exit-code effect for this event type.) This makes it useful only for logging or side effects.

    Exit Codes and Control Flow

    The contract is simple. When a hook script exits with code 0, the action proceeds normally. Exit code 2 is the blocking signal. For PreToolUse, exit 2 blocks the tool call and the script’s stderr is fed to Claude as feedback. For PostToolUse, the tool has already completed, but exit 2 surfaces the script’s stderr to Claude, which can take corrective action. The stderr content becomes part of the agent’s reasoning context, which is what makes hooks corrective rather than merely restrictive. Any other non-zero exit code counts as a non-blocking error, not a block.

    Hook Configuration in .claude/settings.json

    Configure hooks in the hooks object within .claude/settings.json for project-level scope or ~/.claude/settings.json for user-level scope. Each entry is keyed by event name and contains an array of matcher groups. Each group pairs an optional matcher (a pattern matching specific tool names) with a hooks array of one or more handlers, and each handler declares a type (command for a shell command) plus the command to execute.

    {"hooks":{"PreToolUse":[{"matcher":"Write|Edit|MultiEdit","hooks":[{"type":"command","command":"bash scripts/pre-write-gate.sh"}]}],"PostToolUse":[{"matcher":"Write|Edit|MultiEdit","hooks":[{"type":"command","command":"bash scripts/post-edit-lint.sh"}]}]}}

    This skeleton shows the structural pattern. The matcher field accepts pipe-delimited tool names. When a PreToolUse event fires for a Write tool call, Claude Code checks whether any hook entry matches, then executes the command, piping a JSON payload to its stdin containing the tool name and parameters.

    Events and Exit-Code Reference Table with Complete Settings Configuration

    Events and Exit-Code Reference Table

    EventWhen It FiresTool Matcher ExamplesExit 0 BehaviorExit Non-Zero Behavior
    PreToolUseBefore any tool callWrite, Edit, MultiEdit, BashTool proceedsTool blocked on exit 2; stderr shown to Claude
    PostToolUseAfter tool call completesWrite, Edit, MultiEditSilent passExit 2: stderr shown to Claude, may retry
    UserPromptSubmitWhen user submits a new promptN/APrompt proceedsPrompt blocked (rare use)
    NotificationWhen Claude sends a notificationN/ANo effectNo documented effect

    Complete .claude/settings.json with Both Hooks

    The following configuration wires together all three lifecycle events. The PreToolUse entry fires before Write, Edit, or MultiEdit tool calls and blocks the first write attempt per prompt, printing a checklist for Claude to verify before proceeding. The PostToolUse entry fires after those same tool calls complete and runs Prettier (auto-fix) then ESLint (check) on the changed file. If ESLint finds issues, it exits 2 and prints them to stderr, surfacing the lint errors as feedback for Claude to self-correct. The UserPromptSubmit entry resets the pre-write gate so it re-arms on every new user prompt. No matcher is needed for UserPromptSubmit because this event has no associated tool. The reset is delegated to a dedicated script to ensure $PPID is evaluated consistently.

    {"hooks":{"PreToolUse":[{"matcher":"Write|Edit|MultiEdit","hooks":[{"type":"command","command":"bash scripts/pre-write-gate.sh"}]}],"PostToolUse":[{"matcher":"Write|Edit|MultiEdit","hooks":[{"type":"command","command":"bash scripts/post-edit-lint.sh"}]}],"UserPromptSubmit":[{"hooks":[{"type":"command","command":"bash scripts/reset-gate.sh"}]}]}}

    Important: Before relying on the UserPromptSubmit reset, verify that $PPID expands to the same value in this context as it does inside the hook scripts. You can add a temporary diagnostic command — echo "PPID=$PPID" >> /tmp/hook-ppid.log — to both reset-gate.sh and pre-write-gate.sh and compare the logged values. If they differ, the gate reset will not function correctly.

    All three lifecycle events work together as a system: UserPromptSubmit resets state, PreToolUse enforces the gate, and PostToolUse validates output.

    Building the Pre-Write Gate Hook

    What the Gate Does

    On the first file-write attempt after each new user prompt, the hook exits with code 2 and prints a checklist to stderr, which Claude Code feeds to Claude as the reason for the block. The checklist asks Claude to verify three things before writing new code: Does an implementation already exist in the project? Is there a standard-library equivalent? Is there a well-maintained dependency that handles this? This directly addresses the r/Frontend scenario where Claude wrote 1,000 lines duplicating existing functionality.

    After Claude acknowledges the checklist (by seeing the stderr feedback, adjusting its plan, and attempting the write again), subsequent writes within the same prompt pass through. The gate opens for the duration of that conversational turn and re-arms when the user submits a new prompt.

    Managing State with a Temp File

    The stateful pattern uses a temp file to track whether the gate has been opened. The file path follows the pattern /tmp/claude-hook-gate-$PPID. When the file exists, the gate is open and writes proceed. When it does not exist, the gate is closed and writes are blocked.

    The UserPromptSubmit hook entry in settings.json handles the reset by running scripts/reset-gate.sh, which deletes this temp file, so every new user prompt re-arms the gate automatically.

    Note on shared systems: The temp file path is predictable and based on a process ID, which can be a symlink attack surface on multi-user systems. On shared machines, consider using mktemp -d to create a session-specific directory and store the gate file there, or ensure /tmp permissions are appropriately restricted.

    Why PPID and Not PID

    Each hook invocation spawns a new child process, so $PID (or $$) changes on every run. $PPID refers to the parent of the hook script. In the expected case, this is the Claude Code session process, which ties the gate state to the session rather than to the ephemeral script invocation.

    However, this assumption should be verified in your environment. If Claude Code launches hooks through an intermediate shell (e.g., sh -c "bash scripts/..."), $PPID may point to that intermediate shell rather than the Claude Code session, and the value may change between invocations. Before relying on this mechanism, add a diagnostic hook that logs echo "PPID=$PPID" >> /tmp/hook-debug.log and confirm that the value is stable across multiple hook calls within a single session. If it varies, replace $PPID with a fixed session identifier.

    The Script: scripts/pre-write-gate.sh

    #!/usr/bin/env bashset-uo pipefailGATE_FILE="/tmp/claude-hook-gate-$PPID"INPUT=$(cat)read-r TOOL_NAME FILE_PATH <<(printf'%s'"$INPUT"| jq -r'[.tool_name // empty, (.tool_input.file_path // .tool_input.path // empty)] | @tsv')if[[-f"$GATE_FILE"]];thenexit0fiif!touch"$GATE_FILE"2>/dev/null;thenecho"WARNING: Could not create gate file at $GATE_FILE — gate disabled for this session">&2exit0ficat>&2<<EOFPRE-WRITE GATE — Please verify before writing new code:1. Does an implementation of this functionality already existin the project? Search the codebase first.2. Is there a standard-library equivalent in Node.js or thelanguage runtime that handles this?3. Is there a well-maintained dependency already in package.json(or one that should be added) that provides this?File: ${FILE_PATH:-unknown}Tool: ${TOOL_NAME:-unknown}If you have verified all three checks, re-attempt the write.EOFexit2

    This script reads the JSON payload from stdin, extracts the tool name and file path with jq in a single invocation (avoiding double-fork and potential echo mangling), checks for the gate file, and either allows or blocks the action. On the first blocked attempt, it prints the checklist and creates the gate file so the immediate retry succeeds. The UserPromptSubmit hook runs scripts/reset-gate.sh to delete the gate file and re-arm everything for the next prompt.

    Note that set -uo pipefail is used rather than set -euo pipefail. The -e flag is deliberately omitted because it would cause the script to exit immediately on any non-zero subcommand return, potentially before reaching the touch or exit 2 lines. This would cause the gate to silently pass through instead of blocking, defeating the entire mechanism.

    MultiEdit payload caveat: This script assumes the hook payload contains a single .tool_input.file_path key. The MultiEdit tool may pass an array of edits with a different payload structure. Before deploying, verify the actual payload schema by adding printf '%s' "$INPUT" >> /tmp/hook-payload.log before the jq line, triggering a MultiEdit operation, and inspecting the log. Adjust the jq extraction accordingly.

    The Script: scripts/reset-gate.sh

    This script is called by the UserPromptSubmit hook to reset the pre-write gate for the current session. Using a dedicated script ensures $PPID is evaluated in the same way as in pre-write-gate.sh.

    #!/usr/bin/env bashGATE_FILE="/tmp/claude-hook-gate-$PPID"rm-f"$GATE_FILE"

    Testing the Gate Manually

    Verifying the hook works before relying on it in a live session takes three terminal commands. Pipe sample JSON into the script, check the exit code, then observe the behavior change after the gate opens.

    rm-f /tmp/claude-hook-gate-$$echo'{"tool_name":"Write","tool_input":{"file_path":"src/utils.js"}}'|bash scripts/pre-write-gate.shecho"Exit code: $?"echo'{"tool_name":"Write","tool_input":{"file_path":"src/utils.js"}}'|bash scripts/pre-write-gate.shecho"Exit code: $?"rm-f /tmp/claude-hook-gate-$$

    The first run should print the checklist and report exit code 2. The second run, with the gate file now present, should exit silently with code 0. Deleting the gate file simulates what the UserPromptSubmit hook does automatically during a real session.

    Note that $$ is used here instead of $PPID because these commands run directly in the terminal rather than as child processes of Claude Code. This means the PID used for the gate file will differ from production. These manual tests verify the script’s logic (block-then-allow behavior), but they do not verify that $PPID is stable in the actual Claude Code environment. Use the diagnostic hook described in the PPID section above to confirm production behavior.

    Building the Post-Edit Lint and Format Pipeline

    What the Pipeline Does

    Claude has already written the file by the time this hook runs. Prettier auto-fixes formatting, then ESLint checks for lint violations. If ESLint reports issues, the hook exits 2 and prints the errors to stderr, surfacing them as agent feedback; Claude can then issue a follow-up edit to fix them. PostToolUse hooks cannot prevent a write; they can only trigger corrective action after it. If both tools pass, the hook exits silently with code 0.

    Here is what that looks like in practice: Claude writes code, the hook formats it and checks it, and if something fails, Claude immediately sees what went wrong and retries. Prettier’s step is non-blocking (it writes fixes in place), while the linting step surfaces errors for Claude to act on.

    Claude writes code, the hook formats it and checks it, and if something fails, Claude immediately sees what went wrong and retries.

    The Script: scripts/post-edit-lint.sh

    #!/usr/bin/env bashset-uo pipefailINPUT=$(cat)FILE_PATH=$(printf'%s'"$INPUT"| jq -r'.tool_input.file_path // .tool_input.path // empty')if[[-z"$FILE_PATH"]];thenexit0fiPROJECT_ROOT="$(cd "$(dirname"$0")/.."&&pwd)"REAL_PATH="$(realpath -m"$FILE_PATH"2>/dev/null ||true)"if [[ -z "$REAL_PATH" ]] || [[ "$REAL_PATH" != "$PROJECT_ROOT"/* ]]; thenexit 0fi# Use the canonicalized path from here onFILE_PATH="$REAL_PATH"# Only process JavaScript/TypeScript filescase "$FILE_PATH" in*.js|*.ts|*.jsx|*.tsx);;*)exit 0;;esac# Run Prettier to auto-fix formatting (non-blocking)if command -v ./node_modules/.bin/prettier &>/dev/null; then./node_modules/.bin/prettier --write "$FILE_PATH" 2>&1 || truefi# Check that ESLint is available before attempting to run itif ! command -v ./node_modules/.bin/eslint &>/dev/null; thenecho "ESLint not found at ./node_modules/.bin/eslint"echo "Install with: npminstall --save-dev eslint"exit 0fi# Run ESLint and capture output, preserving the exit codeESLINT_EXIT=0LINT_OUTPUT=$(timeout30 ./node_modules/.bin/eslint "$FILE_PATH" --max-warnings 02>&1) || ESLINT_EXIT=$?# Discriminate between ESLint exit codes:#   0   = no issues#   1   = lint violations found#   2   = fatal error (bad config, missing parser, etc.)#   124 = timeout (from the timeout command)case $ESLINT_EXIT in0)exit 0;;1)echo "ESLint lint violations in: $FILE_PATH" >&2echo "" >&2echo"$LINT_OUTPUT">&2exit2;;2)echo"ESLint configuration error for: $FILE_PATH">&2echo"">&2echo"$LINT_OUTPUT">&2exit2;;124)echo"ESLint timed out on: $FILE_PATH">&2exit2;;*)echo"ESLint unexpected exit $ESLINT_EXIT for: $FILE_PATH">&2echo"$LINT_OUTPUT">&2exit2;;esac

    The script canonicalizes the file path using realpath and verifies that it falls within the project root directory. This prevents path traversal attacks where a crafted path like /tmp/../etc/passwd could bypass simple prefix checks. PROJECT_ROOT derives from the script’s own location ($(dirname "$0")/..), which also ensures ESLint and Prettier configs are discoverable.

    Prettier runs with --write to fix formatting in place; its exit code is deliberately ignored with || true because formatting fixes do not block the pipeline. The command -v check ensures both Prettier and ESLint exist locally before attempting to run them. ESLint runs with --max-warnings 0 to treat warnings as errors, enforcing a strict zero-warning policy. The timeout 30 wrapper prevents ESLint from hanging indefinitely on large or problematic files.

    ESLint exit codes are handled distinctly: exit code 1 indicates lint violations (actionable by Claude), exit code 2 indicates a configuration or fatal error (requires developer attention, not a code fix), and exit code 124 indicates a timeout. This distinction prevents Claude from endlessly retrying lint fixes when the actual problem is a broken ESLint configuration. In each failure branch the hook itself exits 2 and writes to stderr, the combination Claude Code surfaces to Claude as feedback.

    Both tools are invokedly installed versions are used. Using npx without –no-install can cause it to silently download packages at runtime if they are not found in local node_modules, introducing network latency, non-determinism, and unexpected cost during hook execution

    Handling Edge Cases

    Files outside the project root, such as /tmp scratch files that Claude sometimes creates during multi-step operations, are skipped by the realpath canonicalization and project-root check. This is more robust than a simple prefix check against /tmp/* or /var/*, because it also catches path traversal patterns like /tmp/../etc/passwd and handles symlinks correctly.

    Missing Prettier or ESLint binaries are handled gracefully. Both steps check for the binary with command -v before running. If ESLint cannot find a config file, it reports a fatal error (exit code 2), and that error flows through stdout to Claude with an explicit “configuration error” label, making it clear that the fix needed is not a code change. This is preferable to silently swallowing the failure.

    Binary files and non-JS/TS files are handled by the extension filter in the case statement. The hook exits with code 0 for any unmatched extension, letting the write proceed without interference.

    Hooks vs. CLAUDE.md Rules: When to Use Each

    Comparison at a Glance

    MechanismEnforcementScopeBest For
    HooksDeterministic (exit code)Project or userHard gates, formatting, linting, security checks
    CLAUDE.mdProbabilistic (prompt)ProjectStyle guidance, architecture rationale, soft preferences

    Layer the two: use CLAUDE.md for the “why” behind conventions, and hooks for the “must.” CLAUDE.md explains that the team prefers existing utilities over new implementations, while the pre-write gate hook enforces that preference. CLAUDE.md describes the formatting standard; the post-edit hook runs the formatter. Neither mechanism replaces the other.

    Best Practices and Pitfalls

    Keep Hook Scripts Fast

    Hooks run synchronously. While a hook executes, the agent is blocked. Aim for under one second per hook invocation. Prettier and ESLint on a single file meet this target on typical developer hardware with local node_modules. Scripts exceeding two to three seconds will noticeably stall the agent loop. Running a full test suite as a hook would not be appropriate.

    Use jq for Safe JSON Parsing

    Hook input payloads follow a JSON structure that may evolve across Claude Code versions. grep or sed parsing is brittle and breaks on structural changes. jq provides safe, schema-aware extraction and handles missing fields gracefully with the // empty fallback pattern used in the scripts above.

    Log Hook Activity for Debugging

    When setting up hooks for the first time, appending output to a log file (>> /tmp/claude-hooks.log) helps diagnose issues with path extraction, exit codes, and unexpected tool names. Remove or disable logging once you have confirmed correct behavior over several sessions, to avoid filling disk space during long runs.

    Version-Control Your Hooks

    Commit the scripts/ directory and .claude/settings.json to the repository. Every team member gets the same guardrails without manual setup. Hooks become part of the project’s development infrastructure, not a personal customization.

    Commit the scripts/ directory and .claude/settings.json to the repository. Every team member gets the same guardrails without manual setup.

    Determinism Where It Matters

    The two-hook setup built in this article addresses both sides of convention drift: the pre-write gate catches duplicated code before it happens, and the post-edit pipeline enforces formatting and lint rules on every change. The pattern extends to security scanning, test runners, or any file-level validation that runs in under a few seconds.

    Sharing our passion for building incredible internet things.

    Build Deterministic Gates Linting
    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Tool Tech Team
    • Website

    Related Posts

    Best LinkedIn Recruiter Alternatives in 2026

    October 23, 2026

    What “Clear Form” Should Really Do: Managing Draft Data in JavaScript

    October 22, 2026

    Working With a Marketing Agency: A Technical Guide for Developers and Designers

    October 22, 2026

    Check if eMudhra CA Is Still Trusted in Your Trust Stores

    October 21, 2026

    Cut Node.js LLM Costs

    October 21, 2026

    Diagnose Browser Blocks and Configure Headers

    October 21, 2026
    Leave A Reply Cancel Reply

    Top posts
    AI Tools

    Lab testing giant Quest Diagnostics confirms patients’ personal and health data was compromised during breach

    By Tool Tech Team
    Tech

    A new subscription service promises an evolving collection of ‘bold and bizarre’ indie games

    By Tool Tech Team
    Business Software

    Oura sues rival Ultrahuman again, threatening its U.S. smart ring comeback

    By Tool Tech Team
    Editors Picks

    Lab testing giant Quest Diagnostics confirms patients’ personal and health data was compromised during breach

    October 23, 2026

    A new subscription service promises an evolving collection of ‘bold and bizarre’ indie games

    October 23, 2026

    Oura sues rival Ultrahuman again, threatening its U.S. smart ring comeback

    October 23, 2026

    Best LinkedIn Recruiter Alternatives in 2026

    October 23, 2026
    About Us

    Welcome to ToolTechBlog, your trusted source for the latest insights, reviews, and practical guides on AI tools, business software, cybersecurity, web hosting, and consumer technology.
    Our mission is simple: to help individuals, entrepreneurs, freelancers, students, and businesses discover the right digital tools to improve productivity, streamline workflows, and make informed technology decisions.

    Our Picks

    Lab testing giant Quest Diagnostics confirms patients’ personal and health data was compromised during breach

    October 23, 2026

    A new subscription service promises an evolving collection of ‘bold and bizarre’ indie games

    October 23, 2026

    Oura sues rival Ultrahuman again, threatening its U.S. smart ring comeback

    October 23, 2026
    Top Reviews

    The AI Hype Index: Unsexy AI

    July 29, 2026

    What it is and How to Fix it

    July 29, 2026

    LG to Ban Residential Proxies from Smart TV Apps

    July 29, 2026

    © 2026 tooltechblog.com. All rights reserved. Designed by DD.

    • About Us
    • Contact Us
    • Terms and Conditions
    • Privacy Policy
    • Disclaimer

    Type above and press Enter to search. Press Esc to cancel.