[FEATURE] Sequential Hook Execution Option

Resolved 💬 3 comments Opened Jan 28, 2026 by coygeek Closed Feb 28, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

Claude Code currently executes all matching hooks in parallel. While this is efficient for independent hooks, it prevents use cases where hooks need to run in a specific order:

  1. Dependent transformations: Hook B needs to process the output of Hook A
  2. Ordered validation: Security check must pass before format check runs
  3. Pipeline processing: Data flows through multiple transformation stages
  4. Priority-based execution: Critical hooks should complete before optional ones start
  5. Resource constraints: Some hooks may conflict if run simultaneously (e.g., both trying to modify the same file)

From the Claude Code hooks reference:

"Parallelization: All matching hooks run in parallel"

There's no option to run hooks sequentially.

Proposed Solution

Add a sequential option to hook matcher configuration.

Configuration

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "sequential": true,
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/format-code.sh"
          },
          {
            "type": "command",
            "command": "/path/to/run-linter.sh"
          },
          {
            "type": "command",
            "command": "/path/to/update-docs.sh"
          }
        ]
      }
    ]
  }
}

Behavior

| Setting | Behavior |
|---------|----------|
| sequential: false (default) | All hooks in the array run in parallel |
| sequential: true | Hooks run one at a time, in array order |

Sequential Execution Details

When sequential: true:

  1. Hooks execute in the order they appear in the hooks array
  2. Each hook must complete (success or failure) before the next starts
  3. If a hook fails (exit code 2), subsequent hooks are skipped
  4. The tool_input or tool_response passed to each hook reflects any modifications from previous hooks
  5. Total timeout is the sum of individual hook timeouts

Output Chaining

For sequential hooks, the output of one hook can optionally be passed to the next:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "sequential": true,
        "chainOutput": true,
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/sanitize-command.py"
          },
          {
            "type": "command",
            "command": "/path/to/validate-command.py"
          }
        ]
      }
    ]
  }
}

When chainOutput: true, each hook receives an additional field:

{
  "previous_hook_output": {
    "exitCode": 0,
    "stdout": "...",
    "updatedInput": {...}
  }
}

Alternative Solutions

  1. Single wrapper script: Combine multiple hooks into one script that runs them sequentially. This works but loses the modularity and individual timeout/error handling of separate hooks.
  1. External orchestration: Use a separate tool to orchestrate hook execution. This adds complexity and doesn't integrate with Claude Code's configuration.
  1. File-based coordination: Hooks write to temporary files to coordinate. This is fragile and error-prone.

Priority

Medium - Would be very helpful

Enables more sophisticated hook workflows without sacrificing modularity.

Feature Category

Configuration and settings

Use Case Example

Scenario: Code quality pipeline

  1. Developer edits a TypeScript file
  2. PostToolUse hook with sequential: true runs:
  • First: prettier --write to format the file
  • Second: eslint --fix to fix linting issues (must run after formatting)
  • Third: tsc --noEmit to type-check (must run after lint fixes)
  1. Each step depends on the previous step's modifications
  2. If formatting fails, linting is skipped (no point running on unformatted code)
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "sequential": true,
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs -I{} sh -c 'if [[ \"{}\" == *.ts ]]; then npx prettier --write \"{}\"; fi'"
          },
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs -I{} sh -c 'if [[ \"{}\" == *.ts ]]; then npx eslint --fix \"{}\"; fi'"
          },
          {
            "type": "command",
            "command": "npx tsc --noEmit"
          }
        ]
      }
    ]
  }
}

Scenario: Security validation chain

  1. User submits a bash command
  2. PreToolUse hook with sequential: true runs:
  • First: Check command against blocklist (fast, critical)
  • Second: Analyze command for injection patterns (slower, thorough)
  • Third: Log command for audit (should only run if previous checks pass)
  1. If blocklist check fails, no point running deeper analysis
  2. Audit log only records commands that passed validation

Scenario: Input transformation pipeline

  1. PreToolUse hook for Write tool:
  • First hook: Normalize line endings (CRLF → LF)
  • Second hook: Strip trailing whitespace
  • Third hook: Ensure final newline
  1. Each transformation builds on the previous
  2. Final modified content is what gets written

Additional Context

Prior Art

Gemini CLI supports sequential execution:

{
  "matcher": "pattern",
  "sequential": true,
  "hooks": [...]
}

Their documentation notes:

"Sequential vs. parallel execution"

as a configuration option for hook matchers.

Implementation Considerations

  • Timeout handling: Total timeout for sequential hooks should be configurable (sum of individual timeouts, or a separate totalTimeout)
  • Error propagation: Clear rules for how errors in one hook affect subsequent hooks
  • Output passing: Decide whether hook outputs are automatically chained or require explicit opt-in
  • Mixed mode: Consider whether a matcher can have some parallel and some sequential hooks
  • Debugging: Sequential execution should be clearly visible in debug output

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗