How to Auto-Format Code After Every Edit With Claude Code Hooks (Step-by-Step)

How to auto-format code after every edit with Claude Code hooks

Claude Code writes good code, but it doesn't always write it in your style. Two-space indents in one file, four in the next, a trailing comma here and not there. You can ask it to run the formatter every time, and it'll forget by the third edit. The fix is a Claude Code hook: a small rule that runs your formatter automatically after every file edit, with no reminder needed. This tutorial walks you through it from an empty folder to a working setup.

Quick answer: Add a PostToolUse hook with the matcher Edit|Write to .claude/settings.json in your project. Its command reads the edited file path from the hook's JSON input with jq -r '.tool_input.file_path' and passes it to your formatter, for example xargs npx prettier --write. It then runs after every edit Claude makes.

What You'll Build

By the end you'll have:

  • ✓ A one-line hook that runs Prettier on every file Claude edits or creates
  • ✓ A small script version that picks the right formatter per language (Prettier, Ruff, gofmt)
  • ✓ A settings file you can commit, so everyone on the project gets the same behavior

When I set this up for a client's codebase, it's usually the very first hook I add. It's harmless, it saves review time, and it shows the team what hooks can do before we try anything that blocks or changes Claude's behavior.

Prerequisites

  • Claude Code installed and working in a terminal inside your project folder.
  • jq, the command-line JSON tool. Anthropic's hook examples use it. Install it with brew install jq on macOS or apt-get install jq on Debian and Ubuntu.
  • A formatter for your language. This guide uses Prettier for JavaScript and TypeScript, plus Ruff and gofmt in the multi-language version.
  • Five minutes and a test file you don't mind Claude rewriting.

How Hooks Work (in 60 Seconds)

A hook is a shell command that Claude Code runs at a specific moment in its lifecycle. Each hook has three parts:

  1. An event: when it fires. PostToolUse fires right after Claude uses a tool, such as editing a file.
  2. A matcher: which tools it applies to. Edit|Write means "only the file-editing tools".
  3. A handler: what runs. Here it's a command that receives details about the tool call as JSON on standard input.

That JSON includes the tool name and the tool's input. For file edits, the input holds file_path, which is exactly what your formatter needs.

Step 1: Pick Where the Hook Lives

Hooks live in Claude Code settings files. Which one you pick decides who gets the hook:

FileApplies toShared?
~/.claude/settings.jsonAll your projectsNo, just your machine
.claude/settings.jsonThis projectYes, commit it to the repo
.claude/settings.local.jsonThis projectNo, kept out of git

For formatting, I use the project file. The formatter config already lives in the repo, so the hook should too. Create the folder if it's missing:

mkdir -p .claude

Step 2: Install Prettier in the Project

Install Prettier as a dev dependency so the hook uses the same version as everyone else:

npm install --save-dev --save-exact prettier

If you already have a .prettierrc, Prettier picks it up automatically. If you don't, it uses its defaults, which is fine for a first test.

Step 3: Add the One-Line Hook

Open .claude/settings.json and add this. If the file already has other settings, merge the hooks key in alongside them.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

Here's what each piece does:

  • PostToolUse runs the hook after the edit has happened, so Prettier sees the final file.
  • Edit|Write limits it to file edits and new files. Bash commands and file reads don't trigger it.
  • jq -r '.tool_input.file_path' pulls the edited file's path out of the JSON that Claude Code sends to the hook.
  • xargs npx prettier --write hands that path to Prettier, which rewrites the file in place.

This is the same pattern Anthropic uses in its own hooks guide, so it's a safe starting point.

Step 4: Check That Claude Code Sees It

Start Claude Code in the project and type /hooks. This opens the hooks browser, where you should see your entry listed under PostToolUse. If it isn't there, check the JSON for a missing comma or bracket, then start a fresh session.

Step 5: Test It With a Messy File

Ask Claude for something you know will need formatting:

Create src/demo.js with a function that adds two numbers.
Use single quotes and no semicolons.

Open src/demo.js after Claude finishes. If your Prettier config uses double quotes and semicolons (the default), you'll see them, even though Claude wrote the opposite. A successful hook shows nothing in the conversation. It just quietly does its job.

Step 6: Upgrade to a Multi-Language Script

The one-liner sends every file to Prettier, including Python or Go files Prettier can't format. On a mixed codebase, a short script is cleaner. Create .claude/hooks/format.sh:

#!/usr/bin/env bash
# Read the hook input and pull out the edited file path
FILE_PATH=$(jq -r '.tool_input.file_path // empty')

# Nothing to format? Exit quietly.
[ -z "$FILE_PATH" ] && exit 0

case "$FILE_PATH" in
  *.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.md)
    npx prettier --write "$FILE_PATH" ;;
  *.py)
    ruff format "$FILE_PATH" ;;
  *.go)
    gofmt -w "$FILE_PATH" ;;
esac

exit 0

Make it executable:

chmod +x .claude/hooks/format.sh

Then point the hook at the script instead of the one-liner. $CLAUDE_PROJECT_DIR is set by Claude Code to your project root, so the path works no matter which folder Claude is in:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh"
          }
        ]
      }
    ]
  }
}

The // empty part of the jq filter returns nothing instead of the word null when there's no file path, so the script exits cleanly instead of trying to format a file called "null". Remove any language line you don't use.

Step 7: Commit It for the Team

Add .claude/settings.json and .claude/hooks/format.sh to git. Anyone who opens the project in Claude Code now gets formatted output without setting anything up. If you'd rather keep the hook to yourself, put the same JSON in .claude/settings.local.json instead.

Troubleshooting

Nothing happens after an edit

Run /hooks and confirm the entry is there. Then check that jq is installed (jq --version) and that your formatter runs on its own in the same terminal.

"hook error" shows up in the transcript

Claude Code reports a non-blocking error when a hook exits with a code other than 0 or 2. Usually the formatter couldn't parse the file or wasn't found. Start Claude Code with claude --debug to see the hook's output, or test the script by hand:

echo '{"tool_input":{"file_path":"src/demo.js"}}' | .claude/hooks/format.sh; echo $?

A 0 at the end means the script succeeded.

The script runs but says "Permission denied"

You skipped chmod +x. Run it again on .claude/hooks/format.sh.

Files changed by Bash commands aren't formatted

That's expected. The Edit|Write matcher only covers Claude's file tools. Anthropic's guide points to a separate FileChanged hook event if you need to catch files changed some other way.

The formatter is slow on big files

Command hooks have a default timeout of 600 seconds, so a slow formatter won't get cut off, but you'll feel the wait after each edit. Format only the one file (as both versions above do), never the whole project.

Where to Go Next

Once auto-formatting works, the same pattern handles lint checks, test runs on changed files, or blocking edits to files like .env. If you want Claude to follow a process rather than run a command, a skill is the better tool: see my guide on building a Claude Skill step by step. For jobs that should run on a timer instead of after an edit, read how to set up a scheduled task in Claude Cowork. And if you're comparing AI tools for your dev pipeline, my roundup of AI DevOps tools covers the wider picture.

Frequently Asked Questions

What is a PostToolUse hook in Claude Code?

It's a hook that runs right after Claude finishes using a tool, such as editing a file or running a command. Because the edit has already happened, it's the right event for formatting, linting or logging.

Where do I put Claude Code hooks?

In a settings file. Use .claude/settings.json to share them with your team through git, .claude/settings.local.json for the current project only, or ~/.claude/settings.json to apply them to all your projects.

Can I use Black or ESLint instead of Prettier?

Yes. The hook only passes the file path to a command. Swap npx prettier --write for any tool that accepts a file path, such as black for Python or npx eslint --fix for JavaScript.

Do hooks slow Claude Code down?

Only by however long your formatter takes on one file, which is usually well under a second. Keep the command focused on the edited file and you won't notice it.

How do I turn a hook off temporarily?

Remove or rename its entry in the settings file. To switch off every hook at once, Anthropic documents a disableAllHooks setting you can set to true.

Want this built for your business?

I'm Basant, an AI automation consultant. Book a call: https://topmate.io/basant_narayan_singh

See more of my work at singhbasant.com.

NextGen Digital... Welcome to WhatsApp chat
Howdy! How can we help you today?
Type here...