Most people use Claude Code like a smarter autocomplete — type a prompt, get some code, repeat. That works. But it’s maybe 20% of what the tool can actually do.
The other 80% lives in three features that most users never touch: CLAUDE.md (how you tell Claude about your project), hooks (how you wire Claude into your existing workflow), and scheduled agents (how you make Claude do things without you being there at all).
This guide covers all three. By the end, you’ll have a Claude Code setup that feels less like a chatbot and more like a junior developer who already knows your codebase.
Part 1: CLAUDE.md — Your Project’s Brain
What It Is
CLAUDE.md is a markdown file you put in the root of your project. Claude Code reads it at the start of every session. Think of it as the briefing document you’d give a new hire on their first day — project context, conventions, things to never do, things to always do.
Without CLAUDE.md, Claude starts every session cold. With it, Claude starts every session knowing exactly what kind of project it’s working on.
Where to Put It
your-project/
├── CLAUDE.md ← project-level (Claude reads this always)
├── src/
│ ├── CLAUDE.md ← subdirectory-level (read when working in src/)
│ └── ...
└── ...
You can have multiple CLAUDE.md files in different directories. Claude reads the ones relevant to whatever files it’s currently working on.
What to Put In It
A good CLAUDE.md has four sections:
1. Project overview — what this thing is and what it does
# Project: acme-store
A Next.js e-commerce storefront for a clothing brand.
Integrates with Shopify Storefront API for products and Stripe for checkout.
2. Tech stack and conventions
## Stack
- Framework: Next.js 15 (App Router)
- Styling: Tailwind CSS
- State: Zustand for cart, React Query for server state
- APIs: Shopify Storefront API, Stripe, Resend for email
## Conventions
- All components use named exports (no default exports)
- API routes live in app/api/, never in components
- Environment variables must be typed in env.ts before use
- All prices stored as integers (cents), never floats
3. Things Claude should always do
## Always
- Run `tsc --noEmit` after any type changes
- Use the existing Button and Input components from ui/ — don't create new ones
- Add loading and error states to any new data-fetching component
- Keep API route handlers under 50 lines — extract logic to lib/
4. Things Claude should never do
## Never
- Install new npm packages without confirming first
- Use `any` as a TypeScript type — find the correct type or use `unknown`
- Add inline styles — use Tailwind classes only
- Commit .env files or API keys
A Real Example
Here’s a CLAUDE.md for a Django API project:
# Project: payments-api
A Django REST Framework API handling subscription billing via Stripe.
Production runs on AWS (ECS + RDS). Staging is on the same stack.
## Stack
- Python 3.12, Django 5.1, DRF 3.15
- PostgreSQL 16 (via RDS in prod, Docker locally)
- Celery + Redis for async tasks
- Stripe SDK for billing
## Conventions
- All views use class-based views (no function-based views)
- Serializers live in serializers.py, never inline in views.py
- Database migrations run automatically in CI — write them carefully
- All monetary values stored as integers (cents), never floats
## Always
- Write tests for any new endpoint (pytest-django, factory_boy for fixtures)
- Add docstrings to any public method
- Use `select_related` / `prefetch_related` to avoid N+1 queries
## Never
- Use `raw()` SQL unless discussing with the team first
- Store secrets in settings.py — use environment variables
- Delete migrations — mark them as squashed if needed
- Touch the Stripe webhook handler without running the full test suite first
When Claude reads this, it knows your entire project context before you type a single word. No more explaining the stack. No more “we use class-based views here.” No more migrations getting deleted by accident.
CLAUDE.md for Sub-Projects
If you have a monorepo or a project with a frontend and backend, put separate CLAUDE.md files in each directory:
my-app/
├── CLAUDE.md ← overall project context
├── backend/
│ └── CLAUDE.md ← Django conventions, DB schema notes
└── frontend/
└── CLAUDE.md ← React conventions, component patterns
Claude reads the closest one to whatever file it’s editing. Working in backend/views.py? It reads backend/CLAUDE.md. Working in frontend/components/Button.tsx? It reads frontend/CLAUDE.md.
Part 2: Hooks — Wiring Claude Into Your Workflow
Hooks let you run shell commands automatically when Claude Code does certain things. They’re defined in .claude/settings.json and trigger on specific events.
The Four Hook Events
PreToolUse → runs before Claude calls a tool (e.g., before it edits a file)
PostToolUse → runs after Claude calls a tool (e.g., after it edits a file)
Notification → runs when Claude sends you a notification
Stop → runs when Claude finishes a task
Basic Hook Setup
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "npm run lint -- --fix"
}
]
}
]
}
}
This runs npm run lint --fix every time Claude writes a file. You never get unlinted code in your repo again.
Practical Hook Patterns
Auto-format after every file write:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH" }
]
}
]
}
}
Run tests after Claude edits a test file:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "if echo '$CLAUDE_TOOL_INPUT_FILE_PATH' | grep -q 'test'; then pytest $CLAUDE_TOOL_INPUT_FILE_PATH -x; fi"
}
]
}
]
}
}
Block Claude from editing certain files:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "if echo '$CLAUDE_TOOL_INPUT_FILE_PATH' | grep -q 'migrations/'; then echo 'ERROR: Do not edit migration files directly.' && exit 1; fi"
}
]
}
]
}
}
If the hook exits with a non-zero code, Claude stops and tells you why. This is a hard guardrail — Claude cannot override it.
Notify you on completion (macOS):
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"'"
}
]
}
]
}
}
Useful when you’ve asked Claude to do something that takes a few minutes and you’ve switched windows.
Hook Environment Variables
Hooks have access to these variables:
| Variable | What It Contains |
|---|---|
$CLAUDE_TOOL_NAME | Which tool Claude called (e.g., “Write”, “Edit”, “Bash”) |
$CLAUDE_TOOL_INPUT_FILE_PATH | Path of the file being written/edited |
$CLAUDE_TOOL_INPUT_COMMAND | The bash command Claude ran (for Bash tool calls) |
$CLAUDE_SESSION_ID | Unique ID for the current session |
Global vs Project Hooks
Hooks in .claude/settings.json (your project directory) apply to that project only.
For hooks that should apply everywhere — like always running a formatter or always getting a notification when Claude stops — put them in ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"Done\" with title \"Claude Code\"'" }
]
}
]
}
}
Part 3: Scheduled Agents — Claude That Runs Without You
This is where things get genuinely interesting. Claude Code can run on a cron schedule — no terminal open, no prompt from you. Just Claude waking up, doing something, and going back to sleep.
Setting Up a Schedule
Add a schedule section to .claude/settings.json:
{
"hooks": {
"schedule": [
{
"name": "Daily dependency check",
"cron": "0 9 * * 1-5",
"prompt": "Check package.json for any dependencies that have major version updates available. Run `npm outdated`. For any package with a major version behind, look up the changelog and write a brief summary of what changed. Save the summary to dependency-updates.md."
}
]
}
}
The cron field uses standard cron syntax:
┌─ minute (0-59)
│ ┌─ hour (0-23)
│ │ ┌─ day of month (1-31)
│ │ │ ┌─ month (1-12)
│ │ │ │ ┌─ day of week (0-7, 0 and 7 = Sunday)
│ │ │ │ │
0 9 * * 1-5 ← 9:00 AM, Monday through Friday
Useful Scheduled Agent Patterns
Weekly code review:
{
"name": "Weekly code review",
"cron": "0 8 * * 1",
"prompt": "Review all files changed in git since last Monday. Look for: unused imports, functions longer than 50 lines, missing error handling in async functions, and any TODO comments older than 2 weeks. Write a report to code-review-YYYY-MM-DD.md with specific file paths and line numbers for each issue found."
}
Daily git summary:
{
"name": "Daily standup notes",
"cron": "30 16 * * 1-5",
"prompt": "Run `git log --since=yesterday --oneline --author=$(git config user.email)`. Summarize today's commits in plain English, grouped by feature area. Write the summary to standup-notes.md. Format it so I can paste it directly into a standup message."
}
Automated test health check:
{
"name": "Test suite health",
"cron": "0 7 * * *",
"prompt": "Run the full test suite with `pytest --tb=short`. If any tests fail, write a file called failing-tests.md with the test names, error messages, and your best guess at the root cause for each. If all tests pass, append a one-line entry to test-log.md with today's date and pass count."
}
SEO audit (the one from our previous tutorial):
{
"name": "Weekly SEO audit",
"cron": "0 8 * * 0",
"prompt": "Run the SEO audit for $SITE_URL. Use crawl.py to collect page data, pipe the output to audit.py, combine both outputs and pipe to report.py. Run notify.py with the path to the generated report file."
}
Keeping Scheduled Prompts Reliable
A few things that make scheduled agents fail silently:
Be explicit about file paths. Scheduled agents don’t have your terminal’s working directory context. Use absolute paths or start with cd /path/to/project &&.
Tell it what to do when things go wrong. If you don’t, Claude will try to recover however it can — which might not be what you want.
{
"prompt": "Run `npm test`. If the command fails with a non-zero exit code, write the full error output to test-failure-$(date +%Y-%m-%d).log and stop. Do not attempt to fix the errors automatically."
}
Cap the scope. Without limits, a scheduled agent might decide to fix every problem it finds. That’s not always what you want at 9 AM on a Monday.
{
"prompt": "Check for dependency updates. Do not make any changes to package.json or run npm install. Only write a report."
}
Putting It All Together
Here’s a complete .claude/settings.json for a web project that uses all three features:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true" }
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo '$CLAUDE_TOOL_INPUT_COMMAND' | grep -qE 'rm -rf|DROP TABLE|git push --force'; then echo 'Blocked: destructive command requires manual confirmation.' && exit 1; fi"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"Task complete\" with title \"Claude Code\"' 2>/dev/null || true" }
]
}
],
"schedule": [
{
"name": "Monday code review",
"cron": "0 8 * * 1",
"prompt": "Review files changed last week. Look for TODO comments, functions over 40 lines, and missing error handling. Write report to weekly-review.md."
},
{
"name": "Daily test check",
"cron": "0 7 * * 1-5",
"prompt": "Run pytest. If any tests fail, write failing-tests.md with the error details. Do not attempt fixes."
}
]
}
}
And the CLAUDE.md that goes with it:
# Project context
[Your project description here]
## Stack
[Your stack here]
## Conventions
[Your conventions here]
## Always
- Check hooks are not being bypassed
- Write tests for new functions
- Use the project's existing patterns, not new ones
## Never
- Run `git push --force`
- Delete migration files
- Use `rm -rf` without confirmation
Quick Reference
| Feature | File | Purpose |
|---|---|---|
| CLAUDE.md | ./CLAUDE.md | Project context, conventions, guardrails |
| Project hooks | ./.claude/settings.json | Automation for this project |
| Global hooks | ~/.claude/settings.json | Automation for all projects |
| Scheduled agents | ./.claude/settings.json under schedule | Cron-based autonomous runs |
The pattern is simple: CLAUDE.md tells Claude what your project is, hooks control what happens when Claude acts, and schedules control when Claude acts. Together they turn Claude Code from a prompt-response tool into something that actively participates in your development workflow.