Skip to content

How to Set Up Lifecycle Hooks for Automated Enforcement in Claude Code

Problem

When I wrote rules in CLAUDE.md like “always run typecheck before committing,” Claude sometimes forgot to follow them. I kept adding more rules, but compliance didn’t improve.

The core issue: rules are text that requests behavior. Claude processes them as suggestions, not guarantees.

The rules problem
Rule in CLAUDE.md: "Always run typecheck before committing"
Claude's behavior: Sometimes remembers, sometimes forgets
Result: Inconsistent code quality

Environment

  • Claude Code (latest)
  • TypeScript project
  • CLAUDE.md with 150+ lines of rules

What happened?

I noticed that rules at line 137 of a 200-line CLAUDE.md were often ignored. When I investigated, I found:

Why rules fail
- LLMs process rules as suggestions, not guarantees
- Long CLAUDE.md files push older rules out of context
- The model may "decide" the rule doesn't apply

One Reddit user put it perfectly: “One of the statistically possible results of saying ‘don’t commit without linting first’ is for the tool to decide to commit without linting first.”

How to solve it?

I replaced rules with lifecycle hooks.

The Hook Types

Claude Code supports three hook types:

Hook TypeWhen It RunsUse Cases
PreToolUseBefore tool executionValidation, blocking dangerous actions
PostToolUseAfter tool executionAuto-formatting, type checking, linting
StopWhen session endsFinal verification, audit logging

Solution #1: PostToolUse Typechecking Hook

I added a hook that runs TypeScript type check after every edit:

~/.claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx tsc --noEmit"
}
]
}
]
}
}

Now Claude doesn’t choose to be typechecked. The environment enforces it.

Solution #2: PreToolUse Git Push Review

I added a hook that opens a diff review before git push:

~/.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'Review changes before push' && code --wait --diff"
}
]
}
]
}
}

Solution #3: Comprehensive Hook Configuration

Here’s my full hook setup:

~/.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "tmux-reminder.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "prettier --write $FILE_PATH"
}
]
},
{
"matcher": "Edit|Write",
"glob": "*.ts",
"hooks": [
{
"type": "command",
"command": "npx tsc --noEmit"
}
]
}
],
"Stop": [
{
"type": "command",
"command": "audit-console-logs.sh"
}
]
}
}

The reason

I think the key reason hooks work better than rules is:

  1. Deterministic enforcement: Hooks run every time, regardless of context
  2. No memory required: The hook doesn’t need Claude to remember it exists
  3. Infrastructure vs documentation: Rules are text hoping for compliance; hooks are code guaranteeing compliance

After implementing hooks, my review time dropped dramatically. By the time I looked at the code, the structural problems were already gone.

Summary

In this post, I showed how to set up lifecycle hooks in Claude Code for automated enforcement. The key point is: rules degrade, hooks don’t. Instead of hoping Claude remembers your rules, build infrastructure that enforces them automatically.

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