AI Coding Assistant Hooks: Making Coding Rules and Quality Checks Enforceable

    This article explains how hooks in AI coding assistants turn natural-language coding rules into runtime-enforced workflows. It covers hook events, matchers, and actions; hook input JSON; exit-code protocols; practical scripts for post-edit checks and high-risk command interception; common automation scenarios; layered configuration; performance, security, and troubleshooting; and the relationship between local hooks and CI quality gates.

    1. What a Hook Is: Why Rule Files Fail

    Many teams document coding conventions such as “always use a strongly typed language” or “automatically format and run static checks after saving code.” These conventions are useful, but AI coding assistants may still fail to follow them during real coding work. The wrong language may be used, required checks may be skipped, or formatting may remain inconsistent.

    The problem is usually not that the rules are unclear. The problem is that rule files are usually soft constraints. They enter the model context as natural language and depend on the model to understand, recall, and execute them proactively. As the context grows longer and tasks become more complex, the chance that rules are ignored increases.

    A hook in an AI coding assistant is one way to turn “suggestions” into “workflow constraints.” A hook automatically runs a user-configured script or command when a specific event occurs. For example, it can run formatting and static checks after a file edit, block high-risk operations before command execution, or add context such as the current branch and pending tasks when the user submits a message.

    Comparison dimension Project convention files Hook mechanism
    Nature Suggestion or reminder Workflow constraint or automatic validation
    Trigger Depends on the model understanding and executing the rule Triggered by the runtime at fixed event points
    Result May be followed, or may be ignored More stable, repeatable, and auditable
    Suitable content Architecture principles, style preferences, decisions requiring judgment Baseline rules, format checks, command interception, context injection
    Analogy Rules posted on a wall Access control, security screening, production-line quality inspection

    It is important to emphasize that hooks improve execution certainty, but they do not make rules absolutely uncircumventable. A hook may not take effect if its configuration is not loaded, the script fails, the hook is temporarily disabled, or the tool itself adopts a fail-open policy. Local hooks are useful for fast feedback, but they cannot replace the final gates in continuous integration and code hosting platforms.

    2. Three Core Elements of a Hook: Event, Matcher, and Action

    A hook rule usually consists of three elements:

    1. Event: When the hook is triggered, such as before a tool call, after a tool call, or when the user submits a message.
    2. Matcher: Which conditions cause the hook to run, such as only for file editing tools, command execution tools, or specific file types.
    3. Action: What happens after the hook is triggered, such as running formatting, static checks, tests, notifications, or blocking operations.

    Using Claude Code-style hook configuration as an example, event names, matcher syntax, and configuration file locations are shown below. Field names may differ across tools, but the design approach is generally the same.

    2.1 Common Events

    Event Trigger point Typical use
    PreToolUse Before a tool call Validate parameters, block dangerous commands, restrict write scope
    PostToolUse After a tool call Format code, run static checks, run type checks, write logs
    UserPromptSubmit When the user submits a message Inject branch, pending tasks, team conventions, or other context
    Stop When the main task ends Generate a summary, send notifications, trigger build validation
    SubagentStop When a subagent task completes Summarize subagent results, notify owners, trigger follow-up workflows
    Permission request events In tools that support these events Automatically allow read-only operations or deny writes to sensitive paths

    A simple rule for choosing events is: use the pre-tool-use event when you want to intercept something before it happens; use the post-tool-use event when you want to validate something after it happens; use the user-prompt-submit event when you want the model to have the latest state.

    2.2 Matchers: Controlling the Scope of a Hook

    In Claude Code-style configurations, a matcher is usually a regular-expression string that matches tool names. For example, to apply only to file write and edit tools, use Write|Edit; to apply only to command execution tools, use Bash.

    The matcher scope should be appropriate. If the scope is too broad, unrelated operations may also trigger scripts and slow down the workflow. If the scope is too narrow, important operations may be missed. A common practice is to first use an observation mode to record logs, then tighten the rules based on actual trigger frequency.

    2.3 A Configuration Example You Can Adapt

    The following example can be placed in a project configuration file. For Claude Code, hooks are usually configured in .claude/settings.json. Common precedence may be enterprise-managed configuration, command-line overrides, project-local configuration, project-shared configuration, and user-level configuration. Exact behavior may vary by version, so the documentation for the tool in use should be treated as authoritative.

    1. {
    2. "hooks": {
    3. "PreToolUse": [
    4. {
    5. "matcher": "Bash",
    6. "hooks": [
    7. {
    8. "type": "command",
    9. "command": "\"$CLAUDE_PROJECT_DIR/.claude/scripts/guard-bash.sh\""
    10. }
    11. ]
    12. }
    13. ],
    14. "PostToolUse": [
    15. {
    16. "matcher": "Write|Edit",
    17. "hooks": [
    18. {
    19. "type": "command",
    20. "command": "\"$CLAUDE_PROJECT_DIR/.claude/scripts/post-edit-check.sh\""
    21. }
    22. ]
    23. }
    24. ]
    25. }
    26. }

    This example means that before the model runs a Bash command, it executes guard-bash.sh to intercept high-risk commands. After the model writes or edits a file, it executes post-edit-check.sh to run formatting, static checks, and type checks.

    3. Hook Input Data: How Scripts Receive Event Information

    Hook scripts usually read event data from standard input. Most implementations pass in a JSON payload, and the script needs to decide whether to continue based on fields in that payload.

    Common fields include:

    1. {
    2. "hook_event_name": "PostToolUse",
    3. "tool_name": "Edit",
    4. "tool_input": {
    5. "file_path": "src/services/user.ts"
    6. },
    7. "tool_response": {},
    8. "session_id": "example-session",
    9. "cwd": "/path/to/project"
    10. }

    Available fields vary by event. For example:

    • PreToolUse and PostToolUse usually contain tool_name and tool_input.
    • For file write or edit tools, tool_input.file_path indicates the target file.
    • For the Bash tool, tool_input.command indicates the command to be executed.
    • Session or subagent events may include fields such as session_id, subagent_type, and output summaries.

    There are two details that are easy to get wrong:

    1. Determine the command type by field, not by event name. For file operations, inspect file_path; for command operations, inspect tool_input.command. Do not rely only on the event name.
    2. File paths may include a line-number suffix. Some tools or editors may return paths such as src/a.ts:12. If a script dispatches by file extension directly, a pattern such as *.ts may fail to match, causing validation to be skipped. The script should first detect and strip the line-number suffix.

    3.1 A Ready-to-Use Post-Edit Check Script

    The following post-edit-check.sh example can be used directly. It reads JSON from standard input, extracts tool_input.file_path, and runs formatting and checks for files such as TypeScript, JavaScript, and Python.

    1. #!/usr/bin/env bash
    2. set -euo pipefail
    3. payload="$(cat)"
    4. # Read JSON from standard input.
    5. file_path="$(
    6. python3 -c '
    7. import sys, json
    8. try:
    9. d = json.load(sys.stdin)
    10. ti = d.get("tool_input") or {}
    11. print(ti.get("file_path", ""))
    12. except Exception:
    13. print("")
    14. ' <<< "$payload" || true
    15. )"
    16. # Exit successfully if no file path is provided.
    17. [ -n "$file_path" ] || exit 0
    18. # Remove a possible line-number suffix, e.g. src/a.ts:12.
    19. if [[ "$file_path" =~ ^(.*):[0-9]+$ ]]; then
    20. file_path="${BASH_REMATCH[1]}"
    21. fi
    22. # Exit successfully if the file does not exist.
    23. [ -f "$file_path" ] || exit 0
    24. hash_file() {
    25. python3 -c '
    26. import sys, hashlib
    27. print(hashlib.sha256(open(sys.argv[1], "rb").read()).hexdigest())
    28. ' "$1"
    29. }
    30. before_hash="$(hash_file "$file_path")"
    31. case "$file_path" in
    32. *.ts|*.tsx|*.js|*.jsx)
    33. if ! npx prettier --write "$file_path" >/dev/null 2>&1; then
    34. echo "Formatting failed. Check the file syntax and try again." >&2
    35. exit 2
    36. fi
    37. if ! npx eslint --fix "$file_path" >/tmp/eslint-report.txt 2>&1; then
    38. echo "Static checks failed. Fix only the issues below; do not rewrite the entire file:" >&2
    39. head -n 30 /tmp/eslint-report.txt >&2
    40. exit 2
    41. fi
    42. if ! npx tsc --noEmit >/tmp/tsc-report.txt 2>&1; then
    43. echo "Type checks failed. Fix the type errors and try again:" >&2
    44. head -n 30 /tmp/tsc-report.txt >&2
    45. exit 2
    46. fi
    47. ;;
    48. *.py)
    49. if command -v ruff >/dev/null 2>&1; then
    50. ruff format "$file_path" >/dev/null 2>&1 || true
    51. if ! ruff check "$file_path" >/tmp/ruff-report.txt 2>&1; then
    52. echo "Python static checks failed. Fix the issues and try again:" >&2
    53. head -n 30 /tmp/ruff-report.txt >&2
    54. exit 2
    55. fi
    56. fi
    57. ;;
    58. *)
    59. exit 0
    60. ;;
    61. esac
    62. after_hash="$(hash_file "$file_path")"
    63. # Compare before and after formatting.
    64. if [ "$before_hash" != "$after_hash" ]; then
    65. echo "The file was modified by formatting tools. Review the diff before continuing, and avoid rewriting the entire file again."
    66. fi
    67. exit 0

    The script should be committed with the project and stored in version control. This ensures that team members receive the same behavior instead of maintaining separate local configurations manually.

    3.2 A Command Interception Script Before Execution

    The following simplified guard-bash.sh is used to intercept high-risk operations before command execution. It identifies the command type through tool_name and tool_input.command.

    1. #!/usr/bin/env bash
    2. set -euo pipefail
    3. payload="$(cat)"
    4. # Read JSON from standard input.
    5. tool_name="$(
    6. python3 -c '
    7. import sys, json
    8. try:
    9. d = json.load(sys.stdin)
    10. print(d.get("tool_name", ""))
    11. except Exception:
    12. print("")
    13. ' <<< "$payload" || true
    14. )"
    15. # Only handle Bash commands.
    16. [ "$tool_name" = "Bash" ] || exit 0
    17. command_text="$(
    18. python3 -c '
    19. import sys, json
    20. try:
    21. d = json.load(sys.stdin)
    22. ti = d.get("tool_input") or {}
    23. print(ti.get("command", ""))
    24. except Exception:
    25. print("")
    26. ' <<< "$payload" || true
    27. )"
    28. case "$command_text" in
    29. *"git push --force"*|*"git reset --hard"*|*"rm -rf /"*|*"DROP TABLE"*|*"TRUNCATE TABLE"*)
    30. echo "This command may damage code history, data, or production environments and has been blocked by a local hook. Use a safe operation or ask a human to confirm." >&2
    31. exit 2
    32. ;;
    33. *)
    34. exit 0
    35. ;;
    36. esac

    4. Exit Codes and Output Protocol: Allow, Block, and Guide

    How a hook reports its result to the AI coding assistant determines whether it can truly enforce constraints. Common protocols use exit codes together with standard output and standard error.

    Exit code Meaning Follow-up behavior
    0 Success Continue normally; standard output may be used as context feedback for the model
    2 Block The reason written to standard error is sent back to the model, which is asked to fix the issue and retry
    Other non-zero codes Non-blocking error Usually logged or allowed to continue, depending on the tool implementation

    A simple way to think about it is: 0 means “validation passed, continue”; 2 means “validation failed, stop and provide the reason”; other error codes usually indicate that the hook itself encountered an error and may not necessarily block the model operation.

    The quality of blocking messages is critical. If a hook only outputs “check failed,” the model may have difficulty fixing the issue accurately. A better approach is to output the problem location, error summary, and repair boundaries:

    1. if ! npx eslint "$file_path" >/tmp/eslint-report.txt 2>&1; then
    2. echo "Static checks failed. Fix only the issues below; do not rewrite the entire file:" >&2
    3. head -n 30 /tmp/eslint-report.txt >&2
    4. exit 2
    5. fi

    By intervention strength, hooks can be divided into three types:

    1. Observe: Only record logs without affecting the workflow. This is suitable for the initial rollout phase.
    2. Warn: Return a reminder message without forcing the model to make changes.
    3. Block: Return exit code 2 and require the model to fix the issue before continuing. This is suitable for baseline rules.

    It is recommended to start with observation mode, collect real trigger data, and then gradually tighten the rules into blocking mode.

    5. Common Automation Scenarios

    5.1 Before File Writes: Control the Entry Point

    • Prevent modifications to production configuration, secret files, and certificate files.
    • Prevent writes to dependency directories, build artifacts, and generated code.
    • Restrict modifications to business code directories to avoid accidental changes to configuration files.
    • In tools that support permission events, automatically allow read-only commands and automatically deny writes to sensitive paths. However, automatic approval turns human confirmation into implicit authorization, so it must be combined with allowlists and the principle of least privilege.

    5.2 After File Writes: Ensure Change Quality

    • Automatically format code to unify indentation, spaces, line breaks, and end-of-file newlines.
    • Run static checks such as ESLint, Pylint, Ruff, and Checkstyle.
    • Run type or compile checks for languages such as TypeScript, Go, and Java.
    • Run related unit tests or component tests for affected files.
    • Keep related files in sync, such as dependency manifests, changelogs, and database migration scripts.

    5.3 Before Command Execution: Block High-Risk Operations

    • Block destructive commands such as rm -rf, force pushes, history resets, and commands that clear database tables.
    • Identify commands that access production environments directly and require switching to pre-production or test environments.
    • Warn about commands run with administrator privileges or commands that download and execute remote scripts.
    • Flag or redact commands that contain secrets, tokens, usernames, or passwords.

    5.4 When the User Submits a Message: Give the Model the Latest State

    • Automatically attach the current branch, working-tree changes, and recent commit records.
    • Inject a summary of team conventions, current iteration tasks, and relevant test requirements.
    • Highlight the modules, owners, and known risks involved in the current task.

    5.5 When a Task or Subagent Task Completes: Wrap-Up Automation

    • Summarize the changes produced in this session and generate a draft commit message.
    • Notify relevant people after a subagent task completes, or trigger follow-up workflows.
    • Automatically trigger build validation and leave the results for the continuous integration pipeline to review.
    • Generate change summaries, test reports, or quality-gate results.

    6. Engineering Practices: Layered Configuration and Maintainability

    Hooks can usually be configured at multiple levels. A common layering is:

    Level Typical location Suitable content
    Enterprise Managed policy or administration console Security baselines, compliance requirements, dangerous-command interception
    Project Configuration inside the project repository Project conventions, formatting, static checks, generated-code protection
    User Personal home-directory configuration Personal notifications, personal shortcut scripts, local experiment tools

    Using Claude Code-style configuration as an example, a common precedence order may be: enterprise-managed configuration is higher than command-line overrides; command-line overrides are higher than project-local configuration; project-local configuration is higher than project-shared configuration; project-shared configuration is higher than user-level configuration. Exact behavior may differ by version, so the actual tool documentation should be followed.

    Regardless of the layering model, the scripts themselves are engineering assets that need maintenance:

    • Include them in version control and review script content through code review.
    • Document the purpose, trigger condition, expected duration, and failure-handling behavior for each hook.
    • Handle exceptional cases such as timeouts, missing files, missing fields, and missing commands.
    • Avoid hardcoding accounts, secrets, tokens, or other sensitive information in scripts.
    • If enterprise teams centrally collect hook logs, document the data scope, storage location, and usage to meet internal data-compliance requirements.

    7. Performance, Security, and Troubleshooting

    7.1 Performance: Do Not Keep Users Waiting at the Gate

    Hooks run in the development workflow, so their execution time directly becomes waiting time. Common optimization methods include:

    • Process only affected files instead of running a full scan.
    • Move full builds and full test suites to session-end hooks or the continuous integration pipeline.
    • Use hash caching on file content and skip checks when the file has not changed.
    • Set timeouts for scripts to prevent individual commands from blocking the entire workflow.
    • Use incremental modes for heavy checks, such as checking only changed files or relevant dependency graphs.

    7.2 Security: Hooks Run on the User’s Machine

    Hooks are essentially local commands automatically executed by the coding assistant, usually with permissions equal to the current user. The following risks should be taken seriously:

    • Only install hooks from trusted sources. Before cloning an unfamiliar repository, inspect its hook configuration and scripts.
    • Do not hardcode secrets, tokens, usernames, or passwords in scripts.
    • Do not return complete sensitive content in blocking messages. Keep only the necessary error summary.
    • stderr or stdout returned to the model may contain injected instructions, creating prompt-injection risk. Hook output should be concise, trustworthy, and auditable.
    • If the tool supports structured output, fields related to “permission decisions” are high-privilege capabilities. Enterprise configurations should limit the permission scope that local scripts can set, preventing scripts from automatically approving dangerous operations.
    • For scenarios with strict isolation requirements, run the AI coding assistant and its hooks in a container or sandbox environment.

    7.3 Troubleshooting: A Checklist for Hooks That Do Not Run

    If a hook appears not to run, check the following in order:

    1. Is the configuration loaded? Confirm that the configuration file path is correct, the format is valid, and it is read by the current project or user configuration.
    2. Does the event match? Confirm that the event name is correct, such as PostToolUse or PreToolUse.
    3. Does the matcher match? Confirm tool-name matching. For example, the file editing tools may be Write or Edit, and the command tool may be Bash.
    4. Is the script executable? Check script permissions, interpreter paths, and the shebang.
    5. Is the input valid? Confirm that the JSON received by the hook is parseable and that field paths are correct.
    6. Did the script time out? Long-running commands may be terminated silently. Shorten the check scope or add logs.
    7. Do the exit codes follow the convention? Confirm that success returns 0 and blocking returns 2, and avoid misusing other error codes.
    8. Temporarily disable a single hook for comparison. Turn off one rule, observe whether behavior changes, and use that to identify the source of the problem.

    7.4 Common Pitfalls

    Infinite loops

    A hook may trigger the post-tool-use event again after modifying a file. If a formatting script repeatedly modifies the same file, or if exit code 2 causes the model to repeatedly rewrite the entire file, the process may enter a loop.

    Recommended solutions:

    • Compare content by bytes or hash before and after formatting. If the content has not changed, allow it to proceed.
    • If formatting actually modifies the file, output a message such as: “The file was modified by formatting tools. Continue based on the latest content and do not rewrite the entire file again.”
    • When blocking, output only the necessary error summary and explicitly require “fix only the issues; do not rewrite the entire file.”
    • If the script must not modify the file again, state this in the blocking message, require the model not to rewrite the file, and ask for a diff or minimal patch.
    • If a temporary marker file is used to prevent repeated execution, handle concurrent writes and cleanup after abnormal process exit.

    False-positive blocks

    Overly strict checks can frequently interrupt the model and slow development. Blocking rules should focus on truly non-negotiable baselines, such as secret leakage, production-environment writes, dangerous command execution, type errors, and serious static-check errors.

    Path differences

    Paths may differ across local, container, and cloud development environments. Scripts should prefer project-root variables provided by the tool, such as $CLAUDE_PROJECT_DIR, instead of hardcoding absolute paths.

    Silent failures

    When a hook is disabled, a script fails, or a timeout occurs, there may be no obvious prompt. It is useful to log execution time, exit code, and output summaries to a logging service and configure monitoring alerts.

    8. Local Hooks and CI Pipeline Gates: Defense in Depth

    The advantage of local hooks is fast feedback, but they run in the developer environment and may be bypassed, temporarily disabled, or inconsistently configured. A mature approach is to combine them with code hosting and continuous integration pipelines to form double protection.

    Protection layer Location Responsibility Trigger timing
    Local hooks Developer environment Immediate feedback, fast interception, automatic formatting On each edit and command execution
    Pipeline gates Code hosting and CI platforms Full checks, builds, tests, and security scans Before code is merged

    If local hooks and pipeline gates disagree, the pipeline gate should usually be the final basis for merging. Local hooks are responsible for finding problems as early as possible, while the pipeline is responsible for the final quality decision.

    The default behavior when a hook fails also needs to be clear. In many implementations, only exit code 2 represents an explicit block. If the configuration is not loaded, the script fails, a timeout occurs, or another non-2 error code is returned, the system may continue by default. Therefore, critical quality gates should not rely only on local hooks. They should also be repeated in the continuous integration pipeline.

    9. Appendix: Common Differences Across Tools

    Implementations may differ across AI coding assistants:

    • Event names may differ. Some tools use Chinese descriptions such as “before tool call” or “after tool call,” while others use names such as PreToolUse and PostToolUse.
    • Matchers may support regular-expression strings, arrays, file paths, or tool types.
    • Configuration may be located in project settings, user settings, enterprise policy, or a graphical interface.
    • Precedence order may change between versions.
    • Exit-code protocols may not be completely consistent. Some tools also support structured JSON output.

    Therefore, implementation should follow the official documentation of the tool being used. It is recommended to start with a low-risk scenario, such as “format after file save,” confirm that events, matchers, input fields, exit codes, and logs work correctly, and then expand to command interception and quality gates.

    10. FAQ: Quick Answers to Common Questions

    1. What is a hook?

    A hook is a script or command automatically executed by an AI coding assistant when a specific event occurs. It does not depend on the model’s temporary memory. Instead, it is triggered by the runtime at a fixed point.

    2. How is a hook different from a rule file?

    Rule files mainly tell the model what it should do and act as soft constraints. Hooks automatically execute checks or operations at key workflow points and provide a more stable form of engineering constraint.

    3. What does exit code 2 mean?

    In common protocols, exit code 2 means blocking. The script stops the current operation and sends the reason from standard error back to the model, asking it to fix the issue and retry.

    4. How do I troubleshoot a hook that does not work?

    Check in order whether the configuration is loaded, whether the event name is correct, whether the matcher matches, whether the script is executable, whether the input JSON is valid, whether it timed out, and whether exit codes follow the convention. You can also temporarily disable a single hook for comparison.

    5. Will hooks slow down development?

    They can if the matcher scope is too broad or if the script runs full checks. You should process only affected files, use incremental checks, caching, and timeouts, and leave full builds and test suites to the continuous integration pipeline.