Skip to content

How to Chain Claude Code Hooks for Automated Workflows

Problem

Every time I edited package.json, I had to manually run the same sequence: npm install, then npm test, then commit the changes. Over and over. Multiply this by dozens of edits per day, and I was losing focus on actual coding.

I knew Claude Code had hooks, but I couldn’t figure out how to chain multiple actions together. The documentation showed single hooks, not pipelines.

Environment

  • Claude Code CLI
  • Node.js project with npm
  • Git repository
  • macOS/Linux shell

What Happened

I started with a simple PostToolUse hook in my settings.json:

settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": ["npm run lint --fix"]
}
]
}
}

This worked for linting, but I needed more. After editing package.json, I wanted:

  1. Run npm install to sync dependencies
  2. Run npm test to verify nothing broke
  3. Only commit if tests passed

My first attempt at chaining failed:

settings.json (broken attempt)
{
"hooks": {
"PostToolUse": [
{
"matcher": "package.json",
"hooks": [
"npm install",
"npm test",
"git add package.json package-lock.json"
]
}
]
}
}

The hooks array runs commands in parallel, not sequentially. Tests started before npm install finished.

Solution

I discovered two approaches: using && for inline chaining, and shell scripts for complex workflows.

Inline Chaining with &&

The simplest approach chains commands with &&:

settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "package.json",
"hooks": ["npm install && npm test && git add package.json package-lock.json"]
},
{
"matcher": "Edit|Write",
"hooks": ["npm run lint --fix", "npm run format"]
}
]
}
}

The && operator ensures each command only runs if the previous one succeeds. If npm install fails, the chain stops.

Shell Scripts for Conditional Logic

For workflows with conditional logic (like “commit only if tests pass”), shell scripts work better:

.claude/hooks/post-edit.sh
#!/bin/bash
set -e
# Run linter
npm run lint --fix
# Run tests
if npm test; then
echo "Tests passed"
# Commit if on feature branch
BRANCH=$(git branch --show-current)
if [[ $BRANCH == feature/* ]]; then
git add -A
git commit -m "Auto-commit: lint and test passed"
fi
else
echo "Tests failed"
exit 1
fi

Then reference it in settings:

settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": ["bash .claude/hooks/post-edit.sh"]
}
]
}
}

Chaining Patterns

PatternUse CaseExample
SequentialLinear workflowsedit - lint - format
ConditionalQuality gatesedit - test - (if pass) commit
ParallelIndependent checksedit - (lint OR format)
File-specificTargeted workflowspackage.json - npm install

For conditional commits:

settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
"npm run test && git add . && git commit -m 'Auto: tests passed'"
]
}
]
}
}

Reason

Hook chaining works because Claude Code treats each hook as a shell command. When you use &&, the shell handles the sequencing, not Claude Code. This means:

  1. Exit codes matter - A non-zero exit code stops the chain
  2. Each command runs in order - && guarantees sequential execution
  3. Environment variables persist - Set once, available throughout the chain

The hooks array runs commands in parallel for speed. But when you need sequence, use a single command with && or call a shell script.

Why This Matters

  • Eliminates repetitive steps - Edit once, run all checks automatically
  • Ensures consistency - Same workflow every time, no skipped steps
  • Catches issues early - Tests run immediately after changes
  • Reduces cognitive load - Focus on code, not process

As one Reddit user put it: “It turns a back-and-forth workflow into something that is less demanding. You set it once and the guardrails are just… there.”

Summary

Chain Claude Code hooks by combining commands with && or using shell scripts. The hooks array runs commands in parallel, but inline && chains them sequentially. For complex conditional logic, extract to a shell script and reference it from your configuration.

Start simple: add one chain, test it, then expand. My current setup handles linting, formatting, testing, and conditional commits automatically. The initial setup took 30 minutes; the time savings compound daily.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments