Source: https://learn.nodegx.io/advanced/hooks
Summary: A hook is a small command Claude Code runs by itself at a set moment. Use one for any rule you have had to repeat, so it fires whether or not Claude remembers it.

# Hooks: Rules Claude Can't Forget

## TLDR

CLAUDE.md and memory are advice. Claude reads them, then decides. A **hook** is a guard. It runs a command of yours at a set moment, such as just before Claude runs a terminal command. It can stop the action before it happens. Claude doesn't get a vote.

Make one when you've corrected the same thing three times. The three I'd make first:

1. **Block the commands that have bitten you.** In my repos that's `git stash`, `git checkout --` and a force push.
2. **Keep Claude out of generated files.** Build output, lock files, anything a tool writes.
3. **Run the check after every edit.** The type checker or linter, so Claude sees the errors while the change is fresh.

A fourth, for long projects: a hook that reads your handoff file into every new session, so `/clear` never starts from nothing.

None of this is needed to build a NodeGX app. NodeGX already checks every change Claude makes before it is saved. Hooks are for the rules of your own that Claude keeps forgetting.

---

## What a hook is

A hook has three parts. The **event** is when it fires. The **matcher** is which tool it applies to. The **command** is what runs. The command gets the details of what Claude is about to do as JSON on its standard input. It answers with an exit code. Zero means carry on. Two means stop, and whatever the command printed to standard error goes back to Claude as the reason.

The events you'll use:

| Event | Fires | Can block? |
|-------|-------|------------|
| `PreToolUse` | Just before Claude uses a tool (Bash, Edit, Write, and so on) | Yes |
| `PostToolUse` | Just after a tool has run | No, but it can hand Claude an error |
| `SessionStart` | When a session starts, resumes, or clears | No, and its output becomes context |
| `PreCompact` | Before a long conversation is squashed into a summary | Yes |
| `Stop` | When Claude finishes a turn | Yes |

There are about thirty events in all. These five cover everything on this page.

Hooks live in the same settings files as everything else:

| File | Applies to |
|------|-----------|
| `~/.claude/settings.json` | You, in every project |
| `.claude/settings.json` | Everyone on this project (it's committed) |
| `.claude/settings.local.json` | You, on this project only (not committed) |

The `/hooks` command shows every hook that's active and where it came from. Plugins can ship hooks too, which is the main reason to read a plugin before you install it. See [Staying safe](https://learn.nodegx.io/setup/staying-safe).

---

## Recipe 1: block the commands that bite

Every project has a short list of commands that have cost you an afternoon. Mine, on a shared checkout, are `git stash`, `git checkout -- file` and `git add -A`. The first two threw away a colleague's uncommitted work. The third swept in files that weren't mine. I had each one written in memory. Claude still ran them, because a memory is something it can forget.

Make a folder `.claude/hooks/` in the project and put this in `guard-git.js`:

```js
// Blocks git commands that have bitten this project. Exit 2 = stop, with the reason on stderr.
let s = '';
process.stdin.on('data', d => (s += d));
process.stdin.on('end', () => {
  const cmd = JSON.parse(s).tool_input?.command || '';
  const rules = [
    [/\bgit\s+stash\b/, 'git stash is banned here: it takes other people\'s uncommitted work. Commit by pathspec instead.'],
    [/\bgit\s+checkout\s+--/, 'git checkout -- discards edits that may not be yours. Restore one file by name, with git restore, after checking who edited it.'],
    [/\bgit\s+add\s+-A\b/, 'git add -A sweeps in files you did not touch. Add files by name.'],
    [/\bgit\s+push\b.*--force\b/, 'No force pushes on this repo.'],
  ];
  for (const [re, why] of rules) {
    if (re.test(cmd)) { process.stderr.write(why); process.exit(2); }
  }
});
```

Then register it in `.claude/settings.json`:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard-git.js\"" }
        ]
      }
    ]
  }
}
```

`$CLAUDE_PROJECT_DIR` is the folder the session started in, so the path works for everyone who clones the repo. Test it by asking Claude to run `git stash`. It should report that the hook stopped it, with your reason.

Write the reason as the instruction you'd give a person. Claude reads it and does the right thing next instead. A bare "blocked" sends it round in circles.

---

## Recipe 2: keep Claude out of generated files

A site I work on builds into `dist/`. Claude edited a file in there twice. The build overwrote it each time. The "fix" vanished. This hook stops any edit under `dist/` before it happens. It's the same shape as recipe 1 with a different matcher, because `Edit` and `Write` put the path at `tool_input.file_path`.

```js
// .claude/hooks/no-generated.js
let s = '';
process.stdin.on('data', d => (s += d));
process.stdin.on('end', () => {
  const p = JSON.parse(s).tool_input?.file_path || '';
  if (/\/(dist|build|node_modules)\//.test(p) || /package-lock\.json$/.test(p)) {
    process.stderr.write(`${p} is generated. Edit the source it comes from, then rebuild.`);
    process.exit(2);
  }
});
```

```json
"PreToolUse": [
  { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/no-generated.js\"" } ] }
]
```

The matcher `Edit|Write` is a list, not a regex. A matcher with only letters, digits and `|` is read as tool names; anything else is read as a regular expression.

**The same recipe protects your checks.** Add `jest.config`, `tsconfig`, `.eslintrc` and `.github/workflows` to the pattern, and Claude can no longer make a failing test disappear by loosening the test. That's a thing it will do when it's stuck. With the hook in place it has to fix the code, or ask you to change the config yourself.

---

## Recipe 3: run the check after every edit

After an edit, run the type checker and feed any errors straight back. `PostToolUse` can't undo the edit. What it does is show Claude the errors while it still remembers what it changed, rather than at the end of the task when the cause is three steps back.

```json
"PostToolUse": [
  {
    "matcher": "Edit|Write",
    "hooks": [
      { "type": "command", "command": "npx tsc --noEmit --pretty false 2>&1 | head -20 >&2; exit 0", "timeout": 60 }
    ]
  }
]
```

Anything written to standard error reaches Claude. Keep the output short or it fills the conversation. That is what the `head -20` is for. If your project's check takes more than a few seconds, put it on `Stop` instead, so it runs once per turn.

::: warning Hooks run with your permissions, every time
A hook is just a command on your computer. A slow one slows every edit. A wrong one fires on every edit. Keep them few, keep them fast, and keep each one's reason to one sentence. If Claude keeps hitting a hook on something that should be allowed, fix the pattern. Don't delete the hook.
:::

---

## Recipe 4: a session that starts where the last one stopped

If you use a handoff file (see [One job per conversation](https://learn.nodegx.io/build/one-job-per-conversation)), this hook reads it into every new session, including after `/clear`. Whatever a `SessionStart` hook prints becomes context Claude starts with.

```json
"SessionStart": [
  {
    "matcher": "startup|clear|compact",
    "hooks": [
      { "type": "command", "command": "cat \"$CLAUDE_PROJECT_DIR/NEXT-SESSION-PROMPT.md\" 2>/dev/null || true" }
    ]
  }
]
```

The matcher names when it fires. `compact` is the one people forget. After a long conversation gets squashed, the handoff comes back in whole. Nothing in it is lost to the summary.

---

## What not to hook

A hook matches a pattern. It can't tell whether this `git stash` is the one bad case or the one good one. So hooks are for rules with no exceptions. Anything that needs judgement stays in CLAUDE.md or memory, where Claude can weigh it. [Claude Code in depth](https://learn.nodegx.io/advanced/claude-code-in-depth) has the test for which is which.

---

## Quick reference

- A hook is a guard: a command that runs at an event and can stop the action with exit code 2.
- Reason on standard error. Write it as the instruction you'd give a person.
- `PreToolUse` on `Bash` reads `tool_input.command`. On `Edit|Write` it reads `tool_input.file_path`.
- Scripts in `.claude/hooks/`, registered in `.claude/settings.json`, path via `$CLAUDE_PROJECT_DIR`.
- `/hooks` lists what's active. Test a hook by asking Claude to do the thing it blocks.
- Few, fast, no exceptions. Judgement calls stay in CLAUDE.md.
