# Paul's Development Blog > Practical agentic development patterns, Claude Code workflows, and agent-ops guidance for developers building software with AI coding agents. ## Blog Posts ## How to Use Git Worktrees: Multiple Branches, Zero Stashing Source: https://paul-schick.com/posts/how-to-use-git-worktrees/ Markdown: https://paul-schick.com/posts/how-to-use-git-worktrees/index.md Description: How to use git worktrees to check out multiple branches at once, plus the exact workflow I use to run parallel Claude Code sessions without conflicts. This blog lives in a single repository: the site content on one side, SEO and analytics tooling on the other. On a typical day, one Claude Code session drafts a post while another works on the analytics side. Both sessions run from the same repository on different branches, and their changes never touch. Git worktrees are what make that clean. A **git worktree** is a built-in Git feature that lets you check out multiple branches of one repository simultaneously. Each checkout lives in its own directory; no stashing, no branch switching, no second clone. Worktrees have been in Git since 2015, but it took coding agents to make them popular. The original use case was something like fixing an urgent bug when you have uncommitted changes in your working directory. With coding agents, people commonly run multiple sessions that would otherwise write to the same directory and race each other. This guide covers how I use worktrees day to day, including the two gotchas that bite everyone the first time. The second half walks through the parallel Claude Code workflow. ## What Is a Git Worktree? A git worktree is an additional working directory linked to the same repository, with its own checked-out branch and its own files on disk. Every worktree shares one `.git` object database, so a commit made in one is immediately visible from all of them. You get multiple checkouts without cloning the repo twice. There is one constraint to know up front. A branch can only be checked out in one worktree at a time. Git enforces this so two directories don't silently fight over the same ref. ## Why Use Git Worktrees? Use git worktrees whenever you need two branches checked out at once. This avoids the conflicts that can be introduced with stashing. A worktree shares the object database with your main checkout and keeps each directory isolated on the filesystem. The most common reasons developers reach for worktrees: | Situation | Why a worktree wins | |---------------------------------|-------------------------------------------------------------------------------------| | Urgent hotfix mid-feature | Fix ships from its own directory; your dirty feature tree never moves | | Long build or test run | The build keeps running in one worktree while you code in another | | Reviewing a teammate's PR | Check out their branch without disturbing your work in progress | | Comparing branches side by side | Two full checkouts, two editor windows, real diffs of running code | | Parallel AI coding agents | Each Claude Code session gets its own files, branch, and dev server - no collisions | That last row is the reason worktrees went from a niche Git feature to a daily tool for agentic development, and it gets its [own section below](#running-parallel-claude-code-sessions-with-worktrees). ## Prerequisites - Git 2.5 or newer (`git --version` to check; anything from the last decade qualifies). The `git worktree remove` subcommand used later in this guide needs 2.17. - An existing repository to work in Worktrees are built into Git core, so there is nothing to install or configure. ## Setting Up Your First Worktree I keep worktrees inside the repository in a dedicated directory, `.claude/worktrees/`. The directory has to be gitignored, otherwise every worktree shows up as a pile of untracked files in every other checkout. So the first step is a one-time addition to `.gitignore`: ```bash echo '.claude/worktrees/' >> .gitignore ``` Then add a worktree for an existing branch: ```bash git worktree add .claude/worktrees/hotfix hotfix/login-bug ``` This creates the directory `.claude/worktrees/hotfix` with `hotfix/login-bug` checked out inside it. If you want to create a new branch at the same time, use `-b`: ```bash git worktree add -b feature/billing .claude/worktrees/billing main ``` That single command creates the branch `feature/billing` from `main` and checks it out in `.claude/worktrees/billing`. Now list what you have: ```bash git worktree list ``` ``` /Users/paul/code/myapp a1b2c3d [main] /Users/paul/code/myapp/.claude/worktrees/hotfix b4c5d6e [hotfix/login-bug] /Users/paul/code/myapp/.claude/worktrees/billing c7d8e9f [feature/billing] ``` Each directory is a fully functional checkout. You commit and push from it exactly as you would anywhere else. A note on where to put git worktrees: they can live anywhere on disk, and older guides usually put them next to the main checkout (`../myapp-hotfix`) or in a home directory like `~/worktrees/`. The gitignored in-repo directory has become the common pattern for agentic development, and `.claude/worktrees/` is also where Claude Code puts the worktrees it creates itself. Everything stays inside the project, which matters when coding agents are sandboxed to the repository directory, and one glance at `.claude/worktrees/` shows what's active. Any of these locations works as long as you're consistent. ## How to Use Git Worktrees The core loop is four commands: 1. **Add**: Run `git worktree add -b ` to create a new branch checked out in its own directory. 1. **Work**: Change into the directory and work normally. Commit, run tests, and push exactly as you would in any checkout. 1. **Remove**: When the branch is merged or abandoned, run `git worktree remove ` to delete the directory and its metadata together. 1. **Prune**: If a worktree directory got deleted manually, run `git worktree prune` to clean up the stale metadata Git left behind. ## Removing Worktrees the Right Way The mistake I see most often is deleting a worktree directory with `rm -rf`. It won't corrupt your repository, but it leaves stale administrative files in `.git/worktrees/`, and Git will still think the worktree exists. The dead worktree keeps showing up in `git worktree list`, and its branch stays reserved, so you can't check that branch out anywhere else. Use the dedicated command instead: ```bash git worktree remove .claude/worktrees/hotfix ``` If you already nuked the directory manually, fix the bookkeeping with: ```bash git worktree prune ``` Git refuses to remove a worktree with uncommitted changes. Pass `--force` if you're sure you want to discard them. One related error is worth knowing. `fatal: '' is not a working tree` means Git has no record of a worktree at the path you gave it, usually because of a typo or because the worktree was already pruned. Run `git worktree list` and copy the exact path from there. ## The Two Gotchas: node_modules and .env Files Worktrees don't copy anything in your `.gitignore`. We need a strategy to manage this reality. **Dependencies don't follow you.** If you have build dependencies, like `node_modules`, these need to be re-generated in each worktree. For a large JavaScript project this also means paying the disk cost per worktree. **.env files don't follow you either.** Since they're gitignored, each worktree starts without secrets or local config. Tests fail and dev servers won't boot. My fix is a tracked `.env.example` with placeholder values, plus a one-line copy when I create a worktree: ```bash cp .env .claude/worktrees/billing/.env ``` Extend this to whatever other files need to be included. Teams with heavier secret requirements pull from a central store instead (1Password CLI, Doppler, dotenv-vault), which removes the copying step entirely. ## Running Parallel Claude Code Sessions with Worktrees If you run AI coding agents, worktrees solve the problem of agents stepping on each other's files. Two Claude Code sessions in one directory is a race condition. Both agents edit the same files, and their changes interleave in ways you can't untangle. Builds run against half-modified code. Each agent gets its own worktree, so it only ever sees its own files and its own branch, with no awareness of the other sessions. The manual setup: ```bash git worktree add -b feature/auth .claude/worktrees/auth main git worktree add -b feature/search .claude/worktrees/search main # Terminal 1 cd .claude/worktrees/auth && claude # Terminal 2 cd .claude/worktrees/search && claude ``` Each session gets a focused prompt for its own feature. The repository's [CLAUDE.md file](/posts/how-to-write-claude-md/) is tracked, so every worktree carries the same project instructions automatically; it's the gitignored local config that gets left behind. While one agent grinds through a refactor, you review the other's diff. When a branch is done, merge it and remove the worktree. I've found two or three parallel sessions is the practical ceiling. For me, using worktrees is most helpful when managing different types of tasks; feature coding in one worktree, and documentation in another, for example. Claude Code also has native support for this pattern, so you often don't need the manual setup at all. Starting a session with `claude --worktree feature-auth` creates the worktree under `.claude/worktrees/` and drops you into it in one step. When you exit, Claude checks for unfinished work before removing the worktree. Sub-agents can be given their own worktrees so parallel edits never conflict. Ask Claude directly to "use worktrees for your agents," or make it permanent for a custom sub-agent by adding `isolation: worktree` to its frontmatter. In the desktop app, every new session gets its own worktree automatically. Claude-created worktrees hit the same `.env` gotcha as manual ones, and here Claude Code has a cleaner fix than the manual copy. Add a `.worktreeinclude` file at the repository root listing the gitignored files you want carried over, and every worktree Claude creates gets a copy automatically. There are also `WorktreeCreate` and `WorktreeRemove` hook events, which I mentioned in my [Claude Code hooks tutorial](/posts/claude-code-hooks-pretooluse-posttooluse/), though those replace the worktree creation logic entirely, mainly for repositories on version control systems other than git. When a `WorktreeCreate` hook is active, `.worktreeinclude` is not processed, so the hook script has to copy any local config files itself. The one thing worktrees don't isolate is shared external state. Two agents pointed at the same database or the same dev server port will still collide. ## When Not to Use Worktrees Worktrees earn their keep when you actually have parallel work, like an agent running in one directory while you code in another. When you don't, they're just overhead. Quick branch hops don't need a worktree, since `git switch` is faster than creating and removing a directory. Repos where installs are expensive and branches share dependencies may be happier with plain stashing. Run `git worktree list` once in a while and clean up. ## Frequently Asked Questions **What is a git worktree?** A git worktree is an additional working directory attached to the same repository, with its own checked-out branch and its own files on disk. All worktrees share one object database, so a commit made in one is immediately visible from the others. You get multiple simultaneous checkouts without a second clone. **How does a git worktree work?** Every worktree shares the repository's object database and refs while keeping its own HEAD, index, and files on disk. Git stores the bookkeeping as lightweight metadata under `.git/worktrees/`. Checking out a branch in one worktree reserves that branch there, so two directories never write to the same ref. **Why use git worktrees instead of a second clone?** A second clone duplicates the full history on disk, and the two copies drift apart until you fetch. A worktree shares the object database with your main checkout, so it is faster to create and lighter on disk. A commit made in one checkout is immediately visible in all the others. **Can two worktrees check out the same branch?** Not by default. Git allows a branch to be checked out in only one worktree at a time, so two directories cannot silently fight over the same ref. `git worktree add --force` overrides the check, but then they really do fight. If you need a second copy of the same code, check out a detached HEAD at that commit instead. **How do I delete a git worktree?** Run `git worktree remove `, which deletes the directory and its metadata together. If you already deleted the directory manually, run `git worktree prune` to clean up the stale metadata. Git refuses to remove a worktree with uncommitted changes unless you pass `--force`. **Do git worktrees copy node_modules and .env files?** No. A new worktree contains only tracked files, and anything in `.gitignore` stays behind, including `node_modules` and `.env` files. Reinstall dependencies in each new worktree and copy your `.env` file in by hand, or pull secrets from a central store. **Can I use git worktrees with Claude Code?** Yes. Claude Code supports worktrees natively. The `--worktree` flag starts a session in a fresh worktree. Ask Claude to use worktrees for its agents, or add `isolation: worktree` to a sub-agent's frontmatter to make the isolation permanent. In the desktop app, every new session gets its own worktree automatically. ## Wrapping Up Git worktrees replace stashing and branch switching with cheap, disposable checkouts. Create them with `git worktree add` and clean them up with `git worktree remove` rather than `rm -rf`. Remember to copy your `.env` file and reinstall dependencies in each new one. If you're running parallel Claude Code sessions, worktrees are what keep them from stepping on each other. For the rest of my day-to-day git toolkit, including the submodule and squash workflows I reach for constantly, see my [git cheat sheet](/posts/git-cheat-sheet/). ## Further Reading - [git-worktree official documentation](https://git-scm.com/docs/git-worktree) - [Run parallel sessions with worktrees, Claude Code docs](https://code.claude.com/docs/en/worktrees) - [Git Worktree: add, list, remove (GitKraken)](https://www.gitkraken.com/learn/git/git-worktree) ## Claude Code Hooks: PreToolUse & PostToolUse Tutorial Source: https://paul-schick.com/posts/claude-code-hooks-pretooluse-posttooluse/ Markdown: https://paul-schick.com/posts/claude-code-hooks-pretooluse-posttooluse/index.md Description: Claude Code hooks block rm -rf, auto-format with Prettier, and protect .env files, deterministically. Copy-paste PreToolUse & PostToolUse examples included. Claude Code follows your CLAUDE.md instructions most of the time. But "most of the time" isn't good enough when the instruction is "never delete .env" or "always run Prettier after editing a file." **Claude Code hooks are event-driven shell commands, LLM prompts, or agentic verifiers that execute deterministically at specific points in Claude Code's tool-call lifecycle. They block destructive actions, enforce code standards, and automate post-edit tasks without relying on the LLM to remember instructions.** Unlike [CLAUDE.md](https://docs.anthropic.com/en/docs/claude-code/claude-md) guidelines that Claude may occasionally skip, hooks fire every time, with no exceptions. This tutorial is for developers and engineering leads who use [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) and want deterministic guardrails around agent behavior. It assumes you have Claude Code installed and have worked with it on at least one project. We focus exclusively on **command hooks** (not prompt or agent hooks) for the two events you'll use most: PreToolUse and PostToolUse. For prompt and agent hook types, see the [Beyond Commands](#beyond-commands-prompt-and-agent-hooks) section. As a software engineer who builds agentic development workflows daily, I've been implementing PreToolUse and PostToolUse hooks across various repositories. This tutorial distills what I've learned into the patterns that matter. By the end, you'll have a working hook configuration that blocks dangerous commands, auto-formats code, and protects sensitive files. > **Key Takeaways** > > - **Hooks are deterministic enforcement.** Unlike CLAUDE.md instructions, hooks fire every time, with no exceptions. > - **PreToolUse** intercepts tool calls before execution. Use it to block destructive commands, protect files, or modify parameters. > - **PostToolUse** reacts after a tool succeeds. Use it to auto-format, lint, or run tests on changed files. > - **Exit code 2** is the magic number. It blocks the action and feeds the error back to Claude. > - Start with three hooks: destructive command blocking, file protection, and auto-formatting. Add more as patterns emerge. ## What Are Claude Code Hooks? Hooks are user-defined event handlers that execute shell commands, LLM prompts, or agentic verifiers at specific points in Claude Code's workflow. They were [introduced in June 2025](https://docs.anthropic.com/en/docs/claude-code/hooks) as part of Anthropic's push toward [agentic coding tools](https://www.anthropic.com/research/swe-bench-sonnet), and have grown from 6 event types to 17 as of early 2026. According to the [Claude Code changelog](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md), major hook capabilities (including `updatedInput` for parameter rewriting and `prompt`/`agent` handler types) shipped across four releases between June and October 2025. The core idea: instead of telling Claude "please format code with Prettier" in your [how to write a CLAUDE.md file](/posts/how-to-write-claude-md) guide and hoping it listens, you configure a PostToolUse hook that runs [Prettier](https://prettier.io/) automatically every time Claude writes a file. The formatter runs whether Claude remembers to or not. Three handler types exist: | Type | What It Does | When to Use | |------|-------------|-------------| | **command** | Runs a shell command | Deterministic checks: formatting, linting, file protection | | **prompt** | Sends a single prompt to an LLM | Judgment calls, e.g. "is this response complete?" | | **agent** | Spawns a multi-turn agent with tool access | Complex verification: run tests and analyze failures | In practice, **command hooks dominate real-world configurations** because most guardrails are deterministic: block a command, format a file, protect a path. Prompt and agent hooks fill the gap when you need LLM judgment. This tutorial focuses on command hooks because they're the ones you should set up first. ## The Hook Lifecycle Here's what happens when Claude Code processes a request: ``` User prompt -> Claude generates tool call (e.g., Edit a file) -> PreToolUse fires (can block, modify, or allow) -> Tool executes -> PostToolUse fires (can format, lint, test, log) -> Claude continues with next action ``` ![Claude Code hook lifecycle showing PreToolUse firing before tool execution and PostToolUse firing after](/claude-code-hook-lifecycle.webp) **PreToolUse** fires after Claude decides what tool to call and with what parameters, but *before* the tool actually runs. You can block the call, modify the parameters, or let it through. **PostToolUse** fires after the tool succeeds. The action already happened, so you can't undo it. But you can react: format the written file, run a linter, trigger a test, log the action. Claude Code supports 17 event types total. Here are the ones worth knowing about: | Event | When It Fires | Can Block? | |-------|--------------|------------| | **PreToolUse** | Before a tool call executes | Yes | | **PostToolUse** | After a tool call succeeds | No | | **Stop** | When Claude finishes responding | Yes | | **SessionStart** | When a session begins or resumes | No | | **UserPromptSubmit** | When you submit a prompt | Yes | | **Notification** | When Claude sends a notification | No | | **SubagentStop** | When a sub-agent finishes | Yes | The full list includes `PostToolUseFailure`, `PermissionRequest`, `SubagentStart`, `TeammateIdle`, `TaskCompleted`, `ConfigChange`, `WorktreeCreate`, `WorktreeRemove`, `PreCompact`, and `SessionEnd`. You don't need all of them. PreToolUse and PostToolUse cover the vast majority of practical use cases in my experience. ## Your First Hook: Configuration Walkthrough Hooks live in [JSON settings files](https://docs.anthropic.com/en/docs/claude-code/settings). You have three options: | File | Scope | Shareable | |------|-------|-----------| | `.claude/settings.json` | This project | Yes (commit to git) | | `.claude/settings.local.json` | This project | No (gitignored) | | `~/.claude/settings.json` | All your projects | No (local to your machine) | For team-shared hooks (formatting, protected files), use `.claude/settings.json`. For personal preferences (notification style, debug logging), use `~/.claude/settings.json`. Here's the structure: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/validate-bash.sh", "timeout": 10 } ] } ] } } ``` The key pieces: - **Event name** (`PreToolUse`): when this hook fires - **matcher**: a regex that filters which tool triggers this hook. `"Bash"` matches the Bash tool. `"Edit|Write"` matches either. `"mcp__.*"` matches all MCP tools. Omit it or use `""` to match everything. - **hooks array**: one or more handlers to run. All matching hooks run in parallel. - **type**: `"command"`, `"prompt"`, or `"agent"` - **command**: the shell command to execute - **timeout**: seconds before the hook is killed (default: 600) Your hook script receives JSON on stdin with context about the event: the tool name, its parameters, the session ID, and the current working directory. To debug hooks, run Claude Code with `claude --debug` or toggle verbose mode with `Ctrl+O` during a session. Without this, hooks fail silently. ## PreToolUse: Intercept Before Execution **PreToolUse matters more than any other hook event for production safety.** It fires after Claude decides on a tool call but before that call executes, giving you a synchronous checkpoint to enforce constraints before any side effects occur. You can: - **Block** the call (exit code 2, or return `permissionDecision: "deny"`) - **Allow** it, bypassing the permission dialog (`permissionDecision: "allow"`) - **Modify** the tool parameters before execution (`updatedInput`, available since v2.0.10) - **Add context** that Claude sees before the tool runs (`additionalContext`) ### Example 1: Block Destructive Shell Commands This hook prevents Claude from running `rm -rf`, `git push --force`, or `DROP TABLE`: ```bash #!/bin/bash # .claude/hooks/block-destructive.sh INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') if [ -z "$COMMAND" ]; then exit 0 fi # Block dangerous patterns DANGEROUS_PATTERNS=( 'rm -rf' 'rm -r /' 'git push --force' 'git push -f' 'DROP TABLE' 'DROP DATABASE' 'git reset --hard' ) for pattern in "${DANGEROUS_PATTERNS[@]}"; do if echo "$COMMAND" | grep -qi "$pattern"; then echo "Blocked: command matches dangerous pattern '$pattern'" >&2 exit 2 fi done exit 0 ``` Hook configuration: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/block-destructive.sh" } ] } ] } } ``` Exit code 2 is critical here. It tells Claude Code this is a **blocking error**: the tool call is prevented and the stderr message is fed back to Claude as an error. Any other non-zero exit code is treated as a non-blocking warning (logged but ignored). This follows the convention documented in the [Anthropic Claude Code hooks reference](https://docs.anthropic.com/en/docs/claude-code/hooks#hook-exit-codes): exit 0 = success, exit 2 = blocking error, any other non-zero = non-blocking warning. Here's what this looks like in practice. Without the hook, Claude silently executes the command: ``` $ claude "clean up the temp directory" > Bash: rm -rf /tmp/project-cache Done. Removed /tmp/project-cache and all contents. ``` With the hook active, the destructive command is caught and Claude adjusts: ``` $ claude "clean up the temp directory" > Bash: rm -rf /tmp/project-cache Hook blocked: command matches dangerous pattern 'rm -rf' > Bash: find /tmp/project-cache -type f -delete Done. Removed files in /tmp/project-cache while preserving the directory. ``` Claude receives the hook's error, understands the constraint, and finds a safer alternative. No manual intervention needed. ### Example 2: Protect Files From Edits Some files should never be modified by Claude: `.env`, lock files, CI config. ```bash #!/bin/bash # .claude/hooks/protect-files.sh INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') if [ -z "$FILE_PATH" ]; then exit 0 fi PROTECTED_PATTERNS=( ".env" "package-lock.json" "pnpm-lock.yaml" "yarn.lock" ".git/" ".github/workflows/" ) for pattern in "${PROTECTED_PATTERNS[@]}"; do if [[ "$FILE_PATH" == *"$pattern"* ]]; then echo "Blocked: '$FILE_PATH' is a protected file" >&2 exit 2 fi done exit 0 ``` ```json { "hooks": { "PreToolUse": [ { "matcher": "Edit|Write|Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/protect-files.sh" } ] } ] } } ``` Note the matcher: `"Edit|Write|Bash"` catches all three tools that could modify files. The Bash tool is included because Claude might use `sed` or `echo >` to write files. ### Example 3: Modify Tool Input Since [v2.0.10](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md), PreToolUse hooks can rewrite tool parameters before execution. This is a significant upgrade over the block-and-retry pattern. Convert a destructive `rm -rf` into an interactive `rm -i`: ```bash #!/bin/bash # .claude/hooks/safer-rm.sh INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') if echo "$COMMAND" | grep -q '^rm -rf'; then SAFER=$(echo "$COMMAND" | sed 's/^rm -rf/rm -ri/') jq -n --arg cmd "$SAFER" '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { command: $cmd } } }' exit 0 fi exit 0 ``` The `updatedInput` field replaces the tool's parameters before execution. Combined with `permissionDecision: "allow"`, the modified command runs without a permission prompt. ## PostToolUse: React After Execution **PostToolUse is the quality enforcement layer.** It runs after every successful tool call, making it the ideal trigger for formatting, linting, and post-edit testing. The action is done. You can't undo a file write or a shell command. But you can: - **Format** the file that was just written - **Lint** the code that was just changed - **Run tests** related to the changed file - **Log** what happened for audit purposes ### Example 1: Auto-Format With Prettier Every file Claude writes gets formatted automatically: ```json { "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true", "statusMessage": "Formatting with Prettier..." } ] } ] } } ``` This is inline, no separate script file needed. It pipes the file path from stdin JSON through `jq`, then passes it to Prettier. The `2>/dev/null || true` ensures non-Prettier-supported files don't cause errors. The `statusMessage` field shows a custom spinner while the hook runs, replacing the default "Running hook..." text. ### Example 2: Run Linter After Code Changes ```bash #!/bin/bash # .claude/hooks/lint-on-change.sh INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') if [ -z "$FILE_PATH" ]; then exit 0 fi # Only lint TypeScript/JavaScript files case "$FILE_PATH" in *.ts|*.tsx|*.js|*.jsx) RESULT=$(npx eslint --fix "$FILE_PATH" 2>&1) if [ $? -ne 0 ]; then echo "$RESULT" >&2 exit 2 fi ;; esac exit 0 ``` When exit code 2 is returned from a PostToolUse hook, the error message is fed back to Claude. Claude sees the [ESLint](https://eslint.org/) errors and can fix them in its next turn, creating an automatic fix loop. ### Example 3: Trigger Related Tests ```bash #!/bin/bash # .claude/hooks/auto-test.sh INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') if [ -z "$FILE_PATH" ]; then exit 0 fi # Find and run the matching test file case "$FILE_PATH" in *.test.ts|*.test.tsx|*.spec.ts|*.spec.tsx) npx vitest run "$FILE_PATH" --reporter=verbose 2>&1 ;; *.ts|*.tsx) TEST_FILE="${FILE_PATH%.ts}.test.ts" if [ -f "$TEST_FILE" ]; then npx vitest run "$TEST_FILE" --reporter=verbose 2>&1 fi ;; esac exit 0 ``` This runs the corresponding [Vitest](https://vitest.dev/) test file whenever Claude edits a source file. No other hook pays back its setup time faster. Claude gets immediate feedback on whether its changes broke something. ## Beyond Commands: Prompt and Agent Hooks Command hooks handle deterministic checks. But sometimes you need judgment. **Prompt hooks** send a single question to an LLM: ```json { "hooks": { "Stop": [ { "hooks": [ { "type": "prompt", "prompt": "Review the conversation context: $ARGUMENTS\n\nAre all requested tasks complete? Respond with JSON: {\"ok\": true} to stop, or {\"ok\": false, \"reason\": \"what's missing\"} to continue.", "timeout": 30 } ] } ] } } ``` This Stop hook uses a fast model (Haiku by default) to evaluate whether Claude's work is actually done before stopping. If the evaluator finds incomplete work, Claude continues. **Agent hooks** spawn a multi-turn agent with Read, Grep, and Glob access: ```json { "hooks": { "Stop": [ { "hooks": [ { "type": "agent", "prompt": "Run the test suite and verify all tests pass. If any fail, explain what's wrong. $ARGUMENTS", "timeout": 120 } ] } ] } } ``` Use command hooks for formatting, linting, and file protection. Use prompt hooks for quick judgment calls. Use agent hooks for complex verification that requires reading files and running multiple commands. ## Hooks vs CLAUDE.md vs Skills Three mechanisms configure Claude Code's behavior. Each serves a different purpose: | | CLAUDE.md | Hooks | Skills | |---|---|---|---| | **Nature** | Suggestion | Enforcement | Contextual expertise | | **Deterministic?** | No | Yes | No | | **When it applies** | Every session | On matching events | When context matches | | **Best for** | Guidelines, preferences | Rules, automation | Reusable workflows | | **Example** | "Prefer Bun over npm" | "Block rm -rf" | "Deploy to staging" | **The simplest rule: if violating the instruction would cause damage, use a hook. If violating it is merely suboptimal, use CLAUDE.md.** The decision is straightforward: - **Preference?** -> CLAUDE.md. "Use named exports." Claude usually follows it. - **Must never be violated?** -> Hook. "Never edit .env." The hook blocks it every time. - **Reusable expertise?** -> Skill. "Deploy with these steps." Activates when relevant. For a deeper dive on CLAUDE.md configuration, see [How to Write the Perfect CLAUDE.md File](/posts/how-to-write-claude-md). ## Five Common Mistakes **1. Wrong exit code.** Exit code 2 blocks. Exit code 1 is a non-blocking warning. If your hook isn't preventing anything, check your exit code. I spent two hours wondering why my file-protection hook was "broken" because Claude kept editing `.env` no matter what. I added `echo` statements, rewrote the pattern matching twice, and finally ran `claude --debug` only to see `hook exited with code 1 (non-blocking)` in the output. Changed `exit 1` to `exit 2` and it worked instantly. **2. Unquoted shell variables.** `$VAR` breaks on spaces. Always use `"$VAR"`. This is especially important when handling file paths from [jq](https://jqlang.github.io/jq/) output. **3. Overly broad matchers.** A PreToolUse hook matching all tools fires on every Read, Glob, and Grep call, slowing everything down. I measured this in one project: an empty-matcher hook added ~80ms per tool call, and Claude makes 15-30 tool calls per prompt. That's 1-2 seconds of overhead per interaction, compounding across a session. Match only the tools you need: `"Bash"`, `"Edit|Write"`, or specific MCP tool patterns like `"mcp__github__.*"`. **4. Missing `stop_hook_active` check.** If your Stop hook tells Claude to keep working, and Claude's next response triggers the Stop event again, you get an infinite loop. I hit this on my first prompt-based Stop hook. Claude generated 11 responses before I killed the session. Always check the `stop_hook_active` field in Stop hook input and exit 0 if it's true. **5. Not testing with `--debug`.** Hooks fail silently by default. Run `claude --debug` to see full hook execution output, or toggle verbose mode with `Ctrl+O` during a session. This is the first thing to try when a hook isn't working. ## What Changes With Hooks Guardrails for AI coding agents are not optional. The [2025 Stack Overflow Developer Survey](https://survey.stackoverflow.co/2025/) found that 84% of developers use or plan to use AI tools, yet only 33% trust the accuracy of AI-generated output. Two-thirds reported frustration with "AI solutions that are almost right, but not quite." Hooks bridge that gap by enforcing review constraints programmatically. After six months of running the three core hooks across four TypeScript and Python projects, here's what I've measured: | Metric | Before Hooks | After Hooks | |--------|-------------|-------------| | Accidental destructive commands per month | 2-3 | 0 | | Manual Prettier/Black runs per session | 5-10 | 0 | | `.env` or lock file edits caught in review | ~1/week | 0 (blocked at source) | | Average hook overhead per tool call | N/A | ~60ms | | Time to set up hooks for a new project | N/A | 10 minutes | ## Quick-Start Configuration Here's a complete `.claude/settings.json` with three practical hooks. Copy it, adjust the paths, and you're running: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/block-destructive.sh", "timeout": 5 } ] }, { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/protect-files.sh", "timeout": 5 } ] } ], "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true", "statusMessage": "Formatting..." } ] } ], "Notification": [ { "matcher": "", "hooks": [ { "type": "command", "command": "notify-send 'Claude Code' 'Needs your attention'" } ] } ] } } ``` Create the hook scripts referenced above (`block-destructive.sh`, `protect-files.sh`), make them executable with `chmod +x`, and restart Claude Code. The hooks take effect on the next session. Type `/hooks` inside Claude Code to see all active hooks and their sources. ## FAQ ### Do hooks work with sub-agents? Yes. Hooks fire for tool calls made by sub-agents, not just the main session. A PreToolUse hook blocking `rm -rf` applies to sub-agent Bash calls too. ### Can I modify tool input with hooks? Yes, since v2.0.10 (October 2025). Return `updatedInput` in your PreToolUse hook's JSON output to change tool parameters before execution. ### What happens if a hook times out? The hook is killed and treated as a non-blocking failure. Claude continues as if the hook didn't exist. Set appropriate timeouts (5-10 seconds for simple checks, 30+ seconds for LLM-based hooks). ### Where should I put hook scripts? `.claude/hooks/` in your project root. This keeps them version-controlled alongside your settings. Use `"$CLAUDE_PROJECT_DIR"` in hook commands for reliable path resolution. ### Can hooks run asynchronously? Yes. Set `"async": true` on a command hook to run it in the background. Claude continues immediately. Async hooks can't block or modify tool calls. They're for logging, notifications, and background tasks. ### How do I debug hooks that aren't firing? 1. Run `claude --debug` to see hook execution logs 1. Check your matcher regex (it's case-sensitive) 1. Verify the hook script is executable (`chmod +x`) 1. Check the JSON structure in your settings file. A missing comma breaks everything. 1. Type `/hooks` to verify Claude Code loaded your hooks ### What is a Claude Code hook? A Claude Code hook is a user-defined event handler (a shell command, LLM prompt, or agentic verifier) that fires automatically at a specific point in Claude Code's tool-call lifecycle. Hooks enforce rules deterministically, unlike [CLAUDE.md instructions](/posts/how-to-write-claude-md) which the LLM may occasionally skip. Claude Code supports 17 hook events as of early 2026, with PreToolUse and PostToolUse being the most widely used. ### How do I use Stop hooks without creating an infinite loop? Check the `stop_hook_active` field in your Stop hook's stdin JSON. If it's `true`, exit 0 immediately. That means your hook already triggered a continuation and Claude is stopping again. Without this check, your Stop hook tells Claude to keep working, which triggers another Stop event, creating an infinite loop. Here's the guard pattern: ```bash INPUT=$(cat) ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false') if [ "$ACTIVE" = "true" ]; then exit 0 fi # Your actual stop hook logic here ``` ## Start Using Claude Code Hooks Today Claude Code hooks, especially PreToolUse and PostToolUse, are the enforcement layer that turns CLAUDE.md guidelines into guarantees. Start with the three-hook configuration above: blocking destructive commands, protecting sensitive files, and auto-formatting on every write. Those three alone will prevent the most common agent mistakes. Once you're comfortable, explore prompt and agent hooks for judgment-based verification. Add more hooks as you discover patterns in your workflow. Every time Claude makes a mistake that a deterministic check could have caught, that's a new hook waiting to be written. For the foundation that hooks build on, see [how to write a CLAUDE.md file](/posts/how-to-write-claude-md). CLAUDE.md provides the guidelines; hooks provide the guardrails. ## How to Write the Perfect CLAUDE.md File Source: https://paul-schick.com/posts/how-to-write-claude-md/ Markdown: https://paul-schick.com/posts/how-to-write-claude-md/index.md Description: CLAUDE.md is a configuration file that tells Claude Code how to behave. Learn the 6-level hierarchy, get a copy-paste template, and avoid 7 common mistakes. A CLAUDE.md file is a markdown configuration file placed at your project root that tells [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) how to behave - which commands to run, which conventions to follow, and which architectural boundaries to respect. To write an effective CLAUDE.md, include only what Claude cannot infer from the code: build commands, style rules that differ from defaults, architectural boundaries, and verification steps. Keep it under 100 lines, version it with git, and update it every time Claude makes a preventable mistake. This guide covers CLAUDE.md configuration only - what to include and omit, how the six-level hierarchy works, and the seven common mistakes that silently reduce Claude's effectiveness. This guide does not cover [Claude Code skills](https://docs.anthropic.com/en/docs/claude-code/skills), MCP servers, or model selection. **Key Takeaways:** - **CLAUDE.md is a project configuration file** for Claude Code's behavior - not documentation, not a README - Keep your CLAUDE.md **under 100 lines**; use `@` imports and `.claude/rules/` for the rest - Include only what Claude cannot infer from the code itself - build commands, architectural boundaries, verification steps - Treat it like code: version it with git, review it in PRs, and prune it regularly - A well-structured CLAUDE.md typically covers 5 sections (as needed): commands, code conventions, architecture, testing, and verification ## What Is CLAUDE.md? **CLAUDE.md is a markdown configuration file that Claude Code reads at the start of every session.** It contains project-specific instructions that Claude follows across all interactions. Think of it as a `.editorconfig` or `.eslintrc` for AI behavior. The key distinction: CLAUDE.md is instructions you write *for* Claude. This is different from [auto memory](https://docs.anthropic.com/en/docs/claude-code/memory), which contains notes Claude writes *for itself*. Both are loaded into context at session start, but you control CLAUDE.md directly. According to [Anthropic's documentation](https://docs.anthropic.com/en/docs/claude-code/memory), Claude Code loads CLAUDE.md into the system prompt before processing any user input. This means instructions in CLAUDE.md take effect before your first message. ## The CLAUDE.md Hierarchy: Six Levels of Configuration Claude Code reads up to six layers of configuration, from the broadest to the most specific, rather than a single CLAUDE.md file. This layered approach mirrors how tools like Git resolve configuration - global settings provide defaults, while local settings override them. Understanding this hierarchy is essential for teams, because it determines which instructions take precedence. ![Diagram showing the six levels of CLAUDE.md configuration hierarchy, from managed policy at the org level down to auto memory at the individual level](/claude-md-hierarchy-diagram.webp) | Level | Location | Shared With | Purpose | |--------------------|----------------------------------------|-------------|-------------------------------| | **Managed Policy** | `/etc/claude-code/CLAUDE.md` (Linux) | Entire org | IT/DevOps enforced rules | | **Project Memory** | `./CLAUDE.md` or `./.claude/CLAUDE.md` | Team (git) | Shared project instructions | | **Project Rules** | `./.claude/rules/*.md` | Team (git) | Modular, topic-specific rules | | **User Memory** | `~/.claude/CLAUDE.md` | Just you | Personal prefs, all projects | | **Local Memory** | `./CLAUDE.local.md` | Just you | Personal prefs, this project | | **Auto Memory** | `~/.claude/projects//memory/` | Just you | Claude's own notes | More specific configuration levels override broader ones. For example, if the user-level file specifies "use tabs" but the project file specifies "use 2-space indentation," Claude will follow the project-level instruction. A few behaviors worth knowing: - **Parent directories load at launch.** If you run Claude Code in foo/bar/, both foo/CLAUDE.md and foo/bar/CLAUDE.md are read. - **CLAUDE.md files in child directories load only** when Claude accesses files in those directories. This approach keeps the context concise. - **CLAUDE.local.md should be gitignored.** Use it for personal sandbox URLs, preferred test data, or any information you do not want to commit. For most developers, the project-level CLAUDE.md is sufficient. In a [2025 Anthropic engineering blog post](https://www.anthropic.com/engineering/claude-code-best-practices), the Claude Code team recommends starting with a single project-level file and expanding to `.claude/rules/` only when the file exceeds 100 lines or when path-specific rules become necessary. ## What to Include (and What to Leave Out) Many developers make mistakes by writing too little, which makes the file ineffective, or too much, which causes Claude to overlook important instructions. Research on [prompt engineering for large language models](https://arxiv.org/abs/2312.16171) shows that concise, structured instructions consistently outperform verbose, unstructured ones - and this principle applies directly to CLAUDE.md. ### Include **Include commands that Claude cannot infer:** ```markdown # Commands - Build: `npm run build` - Test single file: `npx vitest run path/to/test.ts` - Lint: `npm run lint -- --fix` - Typecheck: `npx tsc --noEmit` ``` **Include style rules that differ from defaults:** ```markdown # Code Style - Use ES modules (import/export), not CommonJS (require) - Prefer named exports over default exports - Use Zod for all API input validation ``` **Document decisions that can't be inferred from the code:** ```markdown # Architecture - API routes live in src/app/api/ (Next.js App Router) - All database access goes through src/lib/db/ - never import Prisma directly in routes - Feature flags are managed in src/config/flags.ts ``` **Mandatory verification commands:** ```markdown # Verification - After code changes, run: `npx tsc --noEmit && npx vitest run` - Before submitting: verify lint passes with `npm run lint` ``` ### What to Leave Out of CLAUDE.md - **Omit information that Claude can determine from the code.** For example, if your project uses TypeScript, Claude will recognize this automatically. - **Do not include standard language conventions.** Claude already recognizes that Go uses gofmt and Python uses snake_case. - **Avoid including lengthy API documentation.** Instead, provide links to documentation, such as: See @docs/api-reference.md for the full API specification. - **Exclude frequently changing information.** Since CLAUDE.md loads every session, volatile data can result in outdated context. **Self-evident practices.** "Write clean code" and "follow best practices" are noise. For each line, ask: *Would removing this cause Claude to make mistakes?* If not, remove it. > **The CLAUDE.md litmus test**: Every line in your CLAUDE.md should prevent a specific, repeatable mistake. If you cannot point to a time Claude got something wrong without that instruction, the line is noise. ## Anatomy of a Great CLAUDE.md Here's an example of a real-world CLAUDE.md file. ```markdown # Project: Acme Dashboard Next.js 15 App Router + TypeScript + Prisma + PostgreSQL. Monorepo managed with Turborepo. This package is `apps/dashboard`. # Commands - Dev server: `turbo dev --filter=dashboard` - Build: `turbo build --filter=dashboard` - Test single file: `npx vitest run path/to/file.test.ts` - Test all: `turbo test --filter=dashboard` - Lint + fix: `npm run lint -- --fix` - Typecheck: `npx tsc --noEmit` - DB migrate: `npx prisma migrate dev` - DB generate: `npx prisma generate` # Code Conventions - Use ES modules, not CommonJS - Named exports only (no default exports) - All API inputs validated with Zod schemas in src/lib/validators/ - Error responses follow the format in src/lib/errors.ts - Prefer server components; only use 'use client' when state/interactivity is required # Architecture - Route handlers: src/app/api/ - DB access: src/lib/db/ only - never import @prisma/client directly in routes - Auth: NextAuth.js v5. Session checks via src/lib/auth.ts - Feature flags: src/config/flags.ts. Check before using any gated feature. # Testing - Use Vitest + React Testing Library - Test files go next to source: `component.tsx` -> `component.test.tsx` - Mock external APIs in tests using msw (already configured in src/test/setup.ts) - Prefer running single tests, not the full suite, for speed # Git Workflow - Branch naming: feature/TICKET-123-short-description - Commit messages: conventional commits (feat:, fix:, chore:) - Always run typecheck and lint before committing - PR description must include "What" and "Why" sections # Verification After making changes, always run: 1. `npx tsc --noEmit` (typecheck) 2. `npx vitest run` (tests for changed files) 3. `npm run lint` (lint check) # Common Gotchas - Prisma client must be regenerated after schema changes (`npx prisma generate`) - The `NEXT_PUBLIC_` prefix is required for client-side env vars - Server actions in src/app/actions/ must use 'use server' directive ``` This example is 58 lines - well under the 100-line target - and covers the five core sections: commands, conventions, architecture, testing, and verification. Avoid generic advice, information that can become outdated, or information inferred from the codebase. ## The /init Command: Your Starting Point Running [`/init`](https://docs.anthropic.com/en/docs/claude-code/cli-usage) will auto-generate a CLAUDE.md file by performing an analysis of your project. This provides a starting point, but does not capture: - Workflow conventions (branch naming, commit rules, etc.) - Architectural rules - Verification steps - Team-specific guidelines Run /init, then spend 15 minutes adding project-specific knowledge. Tell Claude to update its own CLAUDE.md file whenever it makes a mistake. ## Advanced Patterns ### `@`imports: Keep it Clean CLAUDE.md supports importing other files: ```markdown # Architecture See @docs/architecture.md for the full system design. See @docs/api-conventions.md for API response formats. ``` Relative paths resolve from the importing file, not the working directory. Imports can chain up to 5 levels deep. First-time imports trigger an approval dialog. ### `.claude/rules/`: Path-Specific Rules For larger projects, split rules into `.claude/rules/` files: ``` .claude/ rules/ api.md # Rules for API code frontend.md # Rules for frontend code testing.md # Testing conventions ``` Scope rules to paths with frontmatter: ```markdown --- paths: - "src/app/api/**/*.ts" --- # API Rules - All endpoints must validate input with Zod - Return errors using the standard format from src/lib/errors.ts - Include rate limiting for public endpoints ``` ## Why Size Matters: CLAUDE.md and the Context Window Each line in CLAUDE.md uses space in Claude's [200,000-token context window](https://docs.anthropic.com/en/docs/about-claude/models), which also stores conversation history, file contents, and tool results. According to research on [lost-in-the-middle effects in long-context models](https://arxiv.org/abs/2307.03172) (Liu et al., 2023), LLMs are more likely to miss instructions buried in the center of long contexts. A bloated CLAUDE.md pushes critical rules into this "attention dead zone." Claude Code triggers auto-compaction at approximately 83.5% of the context window. Once compaction starts, earlier instructions - including parts of your CLAUDE.md - may be summarized or dropped entirely. **Aim for under 100 lines for most projects.** If additional detail is needed, use `@` imports and `.claude/rules/` to keep the core file concise. In practice, I have found that files between 40-80 lines hit the sweet spot: enough detail to prevent common mistakes, but short enough that Claude follows every instruction reliably. ## Seven Common Mistakes | # | Mistake | Why It Hurts | Fix | |---|---------|-------------|-----| | 1 | **Overstuffing** (300+ lines) | Claude loses track of important rules. If everything is important, nothing is. | Keep under 100 lines; use `@` imports for the rest | | 2 | **Pasting code snippets** | Snippets become outdated after refactoring | Reference files with `@` instead | | 3 | **Using it as a linter** | Duplicates ESLint/Prettier; wastes context tokens | Use static analysis tools; use [hooks](/posts/claude-code-hooks-pretooluse-posttooluse/) for enforcement | | 4 | **Relying solely on `/init`** | Auto-generated file only captures obvious rules | Spend 15 minutes adding project-specific knowledge | | 5 | **Adding task-specific rules** | Migration rules distract when writing CSS | Move domain rules to `.claude/rules/` with path scoping | | 6 | **Writing negative instructions** | "Never use --force" can confuse the model | Use positive framing: "Prefer git push over git push --force" | | 7 | **Not pruning over time** | Models improve; old rules become unnecessary noise | Audit monthly; the Claude Code team trims theirs multiple times per week | ## CLAUDE.md Best Practices: Treat It Like Code The most important CLAUDE.md best practices come down to treating the file as a living, versioned document rather than a one-time setup. This mirrors the [infrastructure-as-code](https://en.wikipedia.org/wiki/Infrastructure_as_code) philosophy: declarative, version-controlled, and continuously refined. - **Update CLAUDE.md after each correction.** If you repeatedly provide the same instruction to Claude, include it in the file. - **Review CLAUDE.md during pull requests.** The Claude Code team uses @.claude references in GitHub PRs to propose updates. If a pull request reveals a missing rule, add it. - **Version it with git.** Commit your CLAUDE.md. Let your team contribute. The file compounds in value as more developers add their hard-won lessons. - **Audit periodically.** Once a month, read through your CLAUDE.md and ask: is Claude still making the mistakes these rules prevent? If not, cut the rule. - **Emphasize critical rules.** If a rule is frequently violated, prefix it with "IMPORTANT:" as this measurably improves adherence. ## Quick-Start Template Here's a minimal template to get started. Copy it, fill in the brackets, and delete what doesn't apply: ```markdown # [Project Name] [One-line description. Tech stack.] # Commands - Build: `[build command]` - Test: `[test single file command]` - Lint: `[lint command]` - Typecheck: `[typecheck command]` # Code Conventions - [Convention that differs from the default] - [Convention that differs from the default] # Architecture - [Key architectural boundary or rule] - [Where important things live] # Verification After changes, run: 1. `[typecheck command]` 2. `[test command]` 3. `[lint command]` ``` Expand it based on the actual mistakes Claude makes in your project, rather than anticipated issues. ## Frequently Asked Questions **What is CLAUDE.md?** CLAUDE.md is a markdown configuration file that Claude Code reads at the start of every session. It contains persistent, project-specific instructions - build commands, code conventions, architectural rules, and verification steps - that shape how Claude behaves in your project. **How long should CLAUDE.md be?** Aim for under 100 lines for most projects. Files exceeding 300 lines cause Claude to lose track of important rules due to context window pressure. Use `@` imports and `.claude/rules/` files to offload detailed or path-specific instructions. **Can I have multiple CLAUDE.md files?** Yes. Claude Code reads up to six levels of configuration: managed policy, project memory, project rules, user memory, local memory, and auto memory. It also reads CLAUDE.md files from parent directories at launch and from child directories on demand. More specific files override broader ones. **Does CLAUDE.md support imports?** Yes. Use `@` references like `See @docs/architecture.md` to import other files. Relative paths resolve from the importing file. Imports chain up to 5 levels deep, and first-time imports trigger an approval dialog. **What is the difference between CLAUDE.md and auto memory?** CLAUDE.md is written by you and contains intentional project instructions. Auto memory is written by Claude itself - notes it saves to `~/.claude/projects//memory/` during sessions. Both are loaded into context at session start, but you directly control only CLAUDE.md. **What should I put in CLAUDE.md?** Include four categories of information: (1) build, test, and lint commands, (2) code style rules that differ from language defaults, (3) architectural boundaries Claude cannot infer from code, and (4) verification steps Claude should run after changes. Omit anything Claude can determine from the codebase itself, such as the programming language or standard conventions. **How is CLAUDE.md different from a system prompt?** CLAUDE.md functions like a persistent system prompt that is automatically loaded at session start, but it lives in your repository as a file you version with git. Unlike one-off system prompts, it persists across sessions, is shared with your team through source control, and supports a six-level hierarchy that allows org-wide, project-level, and personal configuration. **Does CLAUDE.md work with other AI coding tools?** CLAUDE.md is specific to [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview), Anthropic's CLI-based coding agent. Other tools have similar concepts - Cursor uses `.cursorrules`, GitHub Copilot uses [custom instructions](https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot) - but the file formats and loading behaviors differ. The principles in this guide (concise instructions, architectural boundaries, verification steps) apply across tools. ## Start Writing Your CLAUDE.md Today **CLAUDE.md is the highest-leverage file in any Claude Code project.** A concise, well-structured file - typically 40-80 lines - consistently outperforms hours of manual prompting because it provides persistent instructions that eliminate the need to repeat yourself. Here's how to get started in the next 15 minutes: 1. Run `/init` in your project to generate a baseline CLAUDE.md 1. Add your build, test, and lint commands 1. Document 3-5 architectural rules that Claude cannot infer from the code 1. Add a verification section so Claude always checks its own work 1. Commit the file, and refine it every time Claude makes a preventable mistake > **Bottom Line**: Getting the most poductive results from AI coding agents is a balance between providing correct context and avoiding too much information. > CLAUDE.md is the starting point for that concise configuration language. For rules that must never be violated, pair your CLAUDE.md with [Claude Code hooks](/posts/claude-code-hooks-pretooluse-posttooluse/) - deterministic enforcement that fires every time, with no exceptions. ## Git Cheat Sheet Source: https://paul-schick.com/posts/git-cheat-sheet/ Markdown: https://paul-schick.com/posts/git-cheat-sheet/index.md Description: Git cheat sheet covering tags, branches, remotes, rebasing, submodules, filter-branch, and index operations with practical examples. ## Resources If you'd prefer to reference Gists, I've provided the links below: - [Git tag cheatsheet](https://gist.github.com/paulschick/b4aad5235a901bec169ff02c3ddc9c3f) - [Git update-index cheatsheet](https://gist.github.com/paulschick/7f20646baadf2fb4d4c9dddd065bbcd1) - [Git submodule cheatsheet](https://gist.github.com/paulschick/3637f9cf02879f8c4402660969ca3ed9) ## Git Tags - `git tag`: List all tags - `git tag -l "v1.0.*"`: List all tags with a pattern - `git tag `: Create a lightweight tag - `git tag -a -m ""`: Create an annotated tag - `git tag -a -m ""`: Create an annotated tag on a commit - `git push `: Push a tag to remote - `git push --tags`: Push all tags to remote - `git checkout `: Checkout a tag - `git tag -d `: Delete a tag locally - `git push --delete `: Delete a remote tag - `git show `: Show information about a tag - `git tag -s -m ""`: Create a signed tag (must have a GPG setup) - `git tag -v `: Verify a signed tag **Note**: Always use annotated tags for marking releases or important points in history as they are checksummed and can include additional information. ### Git Tagging Strategy Example Here's an example of how Git tags could be used for a blog website. 1. **Release Tags**: Each release can be tagged with a version number. The numbering strategy should be determined to suit your needs, for example `MAJOR.MINOR.PATCH`. 1. **Post Publishing**: Have a tag format for each post that's published. An example of this would be `post/YYYY/MM/DD/post-title`. It might make sense to replicate your URL pattern for posts, or use the post slug in addition to the publish date. 1. **Build Metadata**: Typically used if you have a complicated development and release process that involves multiple stages of testing and deployment. You could use tags in this case to indicate that a version is in testing. Don't over-use tags, they should be meaningful. Think of a tag as a snapshot in time. If you have a simple project, it may not be necessary to use them at all. ## Remotes and Branches ### Git Remote Management - `git remote add [name] [url]`: Create a new connection to a remote repository - `git remote rm [name]`: Remove the connection to a remote repository - `git remote rename [old_name] [new_name]`: Rename a remote connection - `git remote show [name]`: Display information about a remote connection - `git remote update`: Fetch updates from all remotes ### Git Branch Management - `git branch [branch]`: Create a new branch from the current branch without switching - `git checkout -b [branch]`: Create a new branch from the current branch and check it out - `git branch -D [branch]`: Delete a branch - `git branch -M [new name]`: Change the name of the current branch - `git push origin --delete [branch]`: Delete a remote branch - `git fetch origin --prune`: Update indexed remote branches (remove deleted remotes) - `git branch`: List local branches - `git branch -a`: List local and indexed remote branches ### Upstreams An upstream in Git is the default branch in the original repository from which your repository was cloned. This is where you pull changes from to update your project. A typical scenario in which you would use an upstream is contributing to an open source project. You would clone the project, make changes, and then submit a pull request **Forking a Repository and Creating a Pull Request** Fork and clone the forked repository to your machine. Set the upstream to the original repository: ```shell git branch --set-upstream-to=[remote]/[branch] [local-branch] ``` Push your changes to the forked repository and create a pull request. ## Git Rebase A rebase is typically used in interactive mode by providing the `-i` flag. This allows you to squash commits, reorder commits, or remove them. You can also use the `--autosquash` flag to automatically squash commits that have the `squash` or `fixup` prefix. When rebasing, you can rebase the entire branch, a certain number of commits, or by providing a commit hash. ```shell # Rebase the last 3 commits git rebase -i HEAD~3 # Rebase using a commit hash git rebase -i 9fceb02 # Rebase the entire branch git rebase -i [branch] ``` **Squashing an Interactive Rebase** An example of the terminal view when squashing commits: ```shell pick 1fc6c95 do something s 6b2481b do something else s dd1475d changed some things # ... Instructions ``` ## Mirroring While `git push` is used to push a specific branch, `git push --mirror` is used to push all branches and tags. This ensures that the destination is updated to be exactly the same as the source repository. Using `--mirror` will also remove any branches or tags in the destination repository that don't exist in the source repository. You can also clone a repository using the `--mirror` flag. This will create a bare repository that is an exact copy of the source repository. A few cases where mirroring can be useful: - Creating a backup of a repository. - Creating a read-only copy of a repository. - Migrating to a new server. - Continuous deployment. Some systems may use `--mirror` to maintain a copy of the repository on the server. The important point to remember is that a repository cloned with `--mirror` is a *bare clone*, and does not have a working directory. You cannot modify files or make commits. It's simply a *mirror*. ## Git Submodules Think about Git submodules as managing nested repositories. You have an outer (parent) repository and one or more inner (child) repositories. The child repositories are called submodules. When you register the child repository as a submodule with the parent repository, it will register the presence of the submodule in a `.gitmodules` file. ### Creating a Submodule This section explains how to create an register a new submodule in your parent repository. Create your outer repository and make at least one commit: ```shell mkdir parent-repo cd parent-repo git init touch README.md git add . git commit -m "initial commit" ``` From inside the parent repository, create a child repository and add at least one commit: ```shell # from parent-repo directory mkdir child-repo cd child-repo git init touch README.md git commit -m "initial commit" ``` Now we have one repository inside of another. Next, you'll want to register the inner repository as a submodule. ```shell # from parent-repo directory git submodule add ./child-repo ``` This command created the `.gitmodules` file and staged the changes. At this point you can commit the changes to the parent repository. There will be a single file representing the submodule that you've added. ### Cloning a Repo with Submodules If you were to push the `parent-repo` to a remote, you have the ability to clone this repo along with any submodules. If you do a normal `git clone` command, you'll need to manually synchronize and download the submodule with another set of commands. If you would like to clone the parent repo and all submodules, use the following: ```shell git clone --recurse-submodules [url] ``` If you did not use `--recurse-submodules`, you can use the following commands to pull in the submodule content: ```shell git submodule init git submodule update ``` - `git submodule init` initializes the submodules, which copies information from `.gitmodules` to the `.git/config` file. - `git submodule update` actually clones the submodule repositories and checks out the appropriate commits. This is the step that pulls the actual content from the submodule remotes. ### Command Summary - `git submodule add [url] [path]`: Add a new submodule to your project. - `git submodule init`: Initialize your submodule. - `git submodule update`: Fetch all the data from the submodule project. - `git submodule status`: Show the status of your submodule. - `git submodule foreach [command]`: Use this to run a command in each submodule. - `git submodule sync`: Synchronize submodules' remote URL configuration setting. - `git submodule deinit [path]`: Remove a submodule from your project. - `git submodule update --remote [path]`: Update a submodule to the latest commit. - `git clone --recurse-submodules [url]`: Clone a project with all submodules. ## Git Index The Git index is known as the staging area. This is where you can manipulate tracked files. We'll focus on `git update-index` to manage files that are assumed unchanged. **Mark a File as Unchanged** Use this to tell Git to assume that a file has not changed, even if it has. This is useful for modification of files locally without committing the changes. ```shell git update-index --assume-unchanged [file] ``` **Stop Assuming a File is Unchanged** Use this command to tell Git to stop assuming that a file is unchanged and start tracking it again. ```shell git update-index --no-assume-unchanged [file] ``` **View Files Assumed Unchanged** When you want to see a list of all files that are assumed unchanged, use this command. ```shell git ls-files -v | grep '^h' ``` This command will show you a list of all files that are assumed unchanged. The `-v` flag will show you the files that are assumed unchanged. The `grep` command will filter the output to only show files that are assumed unchanged. Files that are assumed unchanged will have an `h` in the first column of the output. **Stop Assuming All Files are Unchanged** The following helper script will stop assuming all files are unchanged. ```shell for file in $(git ls-files -v | grep '^h' | awk '{print $2}'); do git update-index --no-assume-unchanged $file; done ``` **Further Notes** Working with the Git index is a low-level operation. The index is typically interacted with through commands like `git add` and `git reset`. Being aware of `git update-index` operations can give you more control over Git operations. ## Git Filter This is a command that is commonly used to remove sensitive data from a repository. It is a powerful command that can have unintended consequences if used incorrectly. It's important to understand how this command works before using it. `git filter-branch` applies filters to each commit in the branch history. **General Syntax**: ```shell git filter-branch --filter -- ``` **Types of Filters** - `--env-filter`: Allows you to modify environment variables for each commit - `--tree-filter`: Modifies the tree (contents) of each commit - `--index-filter`: Modifies the index (staging area) of each commit. Similar to `--tree-filter`, but faster because it doesn't check out the tree. - `--parent-filter`: Modifies the parent list of each commit - `--msg-filter`: Modifies the commit message of each commit - `--commit-filter`: Modifies the commit itself **Use Cases** 1. **Removing sensitive data** 1. **Changing author information** 1. **Squashing commits** Note that for squashing commits, it's generally advisable to use `git merge --squash` or `git rebase -i` to squash instead of `git filter-branch`. This is because `git filter-branch` rewrites history. Let's break down a command, this one would be used to remove sensitive information. Assume that I've accidentally committed the `.env` file containing credentials. I could simply run the following: ```shell git filter-branch --index-filter 'git rm --cached --ignore-unmatch .env' HEAD ``` When you pass `HEAD` as the final value, you're instructing git to rewrite the history up to the current commit on the current branch. You can also pass a commit hash to rewrite history up to a specific commit. **Fair Warning**: `git filter-branch` should probably be avoided when possible. This is especially true when working on a repo with others. **Alternative Tools** It is recommended to use [git-filter-repo](https://github.com/newren/git-filter-repo/) as an alternative to `git filter-branch`. It is a much faster and safer tool for rewriting history. **Example - change the author information** **Use with caution!** - This will change the author information on all branches ```shell git filter-branch --env-filter ' OLD_EMAIL="oldemail@example.com" CORRECT_NAME="Correct Name" CORRECT_EMAIL="correctemail@example.com" if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ] then export GIT_COMMITTER_NAME="$CORRECT_NAME" export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL" fi if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ] then export GIT_AUTHOR_NAME="$CORRECT_NAME" export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL" fi ' --tag-name-filter cat -- --branches --tags ``` **Command Explained** The --tag-name-filter cat -- --branches --tags portion: This part of the command has two components: - `--tag-name-filter cat`: This is used to rewrite tags. `cat` takes the tag name as the input and outputs the same name. This keeps tag names the same. You would need to use a different command to alter the tag names. - `-- --branches --tags`: Operate on all branches and tags. If you would like to change the author information on a *single branch*, you can modify the command to the following: ```shell git filter-branch --env-filter '...' HEAD ``` ## Related Many of the commands in this cheat sheet - `filter-branch`, `push --force`, `rebase` - are destructive if run carelessly, which matters even more when an AI coding agent is running git for you. See [Claude Code Hooks: PreToolUse & PostToolUse](/posts/claude-code-hooks-pretooluse-posttooluse/) for blocking dangerous commands before they execute. ## Deploying a Next.js app with Docker and Nginx on GCP Source: https://paul-schick.com/posts/deploying-nextjs-with-docker-nginx-on-gcp/ Markdown: https://paul-schick.com/posts/deploying-nextjs-with-docker-nginx-on-gcp/index.md Description: Step-by-step guide to deploying a Next.js app with Docker and Nginx on Google Cloud Platform, secured with Let's Encrypt SSL. In this blog post, we will cover the deployment of a Next.js website or blog using Docker, Nginx, and Let's Encrypt on Google Cloud Platform (GCP). If you do not have a Next.js project that you can work with, pull one of the template projects like the [Next.js blog starter](https://github.com/vercel/next.js/tree/canary/examples/blog-starter) to follow along with. This post won't work with the internals of Next.js, so any application that builds successfully will work here. Aspects of this tutorial will apply to other cloud providers as well. This is one reason why I like to use a VPS, in addition to the cost benefits. For instance, in GCP, you create firewall rules to allow select access to ports. This is equivalent to using `ufw` on a provider like Vultr. ## Resources - [Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/) - [Certbot instructions](https://certbot.eff.org/instructions) - [Certbot documentation](https://eff-certbot.readthedocs.io/en/stable/) - [Certbot Command Line Options](https://eff-certbot.readthedocs.io/en/stable/using.html#certbot-command-line-options) - [Generate SSL Certificate Silently - StackOverflow](https://stackoverflow.com/questions/49172841/how-to-install-certbot-lets-encrypt-without-interaction) - [Fixing Certbot's parsefail error](https://charles-stover.medium.com/fixing-certbots-parsefail-error-during-certificate-renewal-1e7718f8a492) ## Introduction Next.js is a popular React framework that simplifies web application development. Features like server-side rendering and static site generation make Next.js a good choice for static blog website development. In this guide, we'll use Docker to containerize a Next.js application and deploy it with Nginx on Google Cloud Compute Engine. The site will be secured with a free SSL certificate from Let's Encrypt. We'll use a virtual private server (VPS) on Google Cloud's Compute Engine, which can offer significant cost advantages over pre-packaged solutions (like platform-as-a-service, such as Cloud Run) for personal web applications or other projects that don't require advanced processes like automatic scaling or serving a large number of users worldwide. We have two main objectives: 1. Demonstrate how to set up this deployment on Google Cloud Compute Engine. 1. Explain the requirements for deploying on any cloud provider. ## Setting up a Google Cloud Compute Engine Instance ### Creating the Server If you haven't signed up for Google Cloud, you can create an account and receive $300 in free credits. Additionally, this deployment falls under the free tier, allowing you to use it for free for a certain period. Google Cloud Platform (GCP) is user-friendly, with all operations accessible through the `gcloud` CLI or the "Cloud Console" web interface. - Create a new project or use the default project. - Navigate to Compute Engine and enable the Compute Engine API if not already enabled. - Name the instance, and select a region and zone. - Choose a machine type; we recommend using `e2-small`. ![Compute engine instance initial config](/gcp-1-compute-engine.png) ![Boot disk configuration](/gcp-2-boot-disk.png) Configure the firewall rules by allowing HTTPS traffic and adding the `nginx` network tag. Although this tag doesn't have any immediate effect, we'll create a firewall rule later that applies to all instances with this tag. For the IP address, you can reserve an external IP address, or leave it as the default `Ephemeral`. Note that if you don't reserve an IP address, the server's external IP address will change upon restart. Having a static external IP address can be helpful when configuring a domain name to point to this IP address. **Note**: When choosing to allow HTTPS traffic, you are applying a pre-configured firewall rule to allow that particular type of traffic into the VPS. If you don't choose this here, we'll be able to apply the appropriate rules later on when we configure Nginx and Let's Encrypt. ![Compute engine instance firewall configuration](/gcp-3-networking-tags.png) ![Create static IP address for compute engine instance](/gcp-4-create-ip-address-selection.png) ![Compute engine instance network interface](/gcp-5-reserved-ip-network-interface-setup.png) ### Configuring the Firewall Rules The next step is to configure the firewall rules to allow HTTPS traffic to the VPS. This traffic will need to be allowed to access the appropriate ports used by Nginx and Let's Encrypt, specifically 80 and 443 through TCP. The default firewall rule set when creating the instance allows access to port 443 over TCP. This is why applying that at server creation is unimportant, as we're going to do that now. **Additional Note:** If you're using a different cloud provider, like Vultr, you will need to use `ufw` to apply these rules. `ufw` *does not* work with GCP, you must apply the firewall rules through the VPC firewall rules section of the Console, or through the `gcloud` CLI. I will provide the `ufw` commands later on. For GCP, navigate to VPC Network -> Firewall. 1. Name the rule (e.g., `nginx-certbot`). 1. Set it as an Ingress rule, allowing traffic to ports `80` and `443` through TCP. 1. Specify the target tag as `nginx` (or whatever tag you assigned to the instance at creation). 1. Apply `0.0.0.0/0` as the IPv4 range to allow traffic from all IP addresses. ![Create a firewall rule step 1](/gcp-7-create-a-firewall-rule-1.png) ![Create a firewall rule step 2](/gcp-8-create-a-firewall-rule-2.png) Once the firewall rules are configured, you can access the server via SSH using `gcloud`, or through the integrated SSH client on the GCP Console. ### Configuring Firewall Rules Through `ufw` If you are using GCP, this section does not apply. If you're using a provider that allows you to modify firewall rules through the server, then you may use `ufw` with Ubuntu to open the required ports for the application to be accessed over the internet. ```bash sudo ufw allow 80/tcp sudo ufw allow 443/tcp ``` ## Install Dependencies ### Overview - Update and upgrade packages, restart the server - Assume that the virtual machine name is `nextjs-instance` for the following examples. Replace this with whatever you've named your machine. - `gcloud` will be used for examples. Remember to set a default project, region, and zone for the configuration. This allows you to run commands without specifying these parameters, provided that the resource you are attempting to access is within those set parameters. ### VM Initial Upgrades SSH into the virtual machine, upgrade, and restart: 1. Use the `gcloud` CLI or GCP Console's built-in SSH client to SSH into the virtual machine. 1. Run `sudo apt update` to update the package list. 1. Run `sudo apt upgrade` to upgrade the installed packages. 1. Restart the system using `sudo reboot`. ### Install Certbot Dependencies Run the following command to install Certbot and its Nginx plugin: ```bash sudo apt update && sudo apt install -y certbot python3-certbot-nginx uidmap ``` ### Install Docker To install Docker on your Ubuntu system, run the following commands: ```bash curl -fsSL https://get.docker.com -o get-docker.sh sudo sh ./get-docker.sh # allow the installation to complete rm ./get-docker.sh ``` Docker compose will be installed through the convenience script along with Docker Engine. Note: Make sure to check the [Docker Compose release page](https://github.com/docker/compose/releases) for the latest version, and ensure that this script url is up to date. ### Test the installations To verify that Docker and Docker Compose are installed correctly, run the following commands: ```bash docker --version docker-compose --version ``` These commands should return the installed versions of Docker and Docker Compose, respectively. If they do, you're good to go! You now have all the necessary dependencies installed for your deployment. Proceed with configuring your application and deploying it using Docker and Docker Compose. ## Securing Your Website with SSL Certificates **Note**: It's crucial to create a backup of your Let's Encrypt files. By doing so, you can easily apply the generated certificate to a new server without generating a new one, should the need arise. This is particularly important because there is a limit to the number of certificates you can generate for a specific domain name within a certain time frame. The current rate limits for Let's Encrypt can be found [here](https://letsencrypt.org/docs/rate-limits/). To test if you can generate a certificate, run the command below. Once you're ready to generate the certificate, simply remove the `--dry-run` flag. ```bash sudo certbot certonly -n --nginx --agree-tos -m me@example.com -d example.com -d www.example.com --dry-run ``` This will obtain an SSL certificate for your domain to enhance security and build trust with visitors to your site. An SSL cert should be considered a requirement for any modern website. ### Stopping Nginx After generating a certificate and each time the server reboots, Nginx will start at the system level. This occurs because Nginx is added when Certbot is run in this configuration. To free up the port for the Nginx container that will run in Docker, we need to stop Nginx. ```bash sudo systemctl stop nginx;sudo nginx -s stop ``` To prevent Nginx from starting up when the server restarts, you can disable it using the following command: ```bash sudo systemctl disable nginx ``` ### Transferring SSL Certificates to Another Server Backing up or transferring SSL certificates to another server is straightforward, but you must be mindful of the symbolic links created during the process. First, back up the entire `letsencrypt` directory created by Certbot at `/etc/letsencrypt`. When transferring to another server, copy this entire directory to the same location. Replace `example.com` with your domain in these instructions. We need to recreate symbolic links in two key directories: - `/etc/letsencrypt/live/example.com` - `/etc/letsencrypt/archive/example.com` Compare the contents of the `archive/example.com` directory with those of the `live/example.com` directory. If you have one set of files, the archive files will have `1` appended to their names. Create a symbolic link for each file in `archive/example.com` to `live/example.com`, removing that number from the symbolic link name. ```bash LIVE="/etc/letsencrypt/live/example.com" ARCHIVE="/etc/letsencrypt/archive/example.com" # Remove files from live/example.com # Create sym links from archive/example.com to live/example.com cd "$LIVE" rm cert.pem chain.pem fullchain.pem privkey.pem ln -s "$ARCHIVE/cert1.pem" "$LIVE/cert.pem" ln -s "$ARCHIVE/chain1.pem" "$LIVE/chain.pem" ln -s "$ARCHIVE/fullchain1.pem" "$LIVE/fullchain.pem" ln -s "$ARCHIVE/privkey1.pem" "$LIVE/privkey.pem" ``` By following these steps, you'll be able to easily transfer SSL certificates without worrying about hitting the rate limit. ## Containerizing the Next.js Application To containerize the Next.js application, create a Dockerfile in the same directory as the project. As we'll be using Docker Compose, it's helpful to have a parent directory above the Next.js project. ### Project Structure Our project structure should resemble the following: ```markdown . |-- docker-compose.yml |-- nextjs-app | `-- Dockerfile `-- nginx `-- nginx.conf ``` ### Creating the Dockerfile In the `nextjs-app` directory, create a `Dockerfile` with the following contents: ```bash # Specify the base image FROM node:18-alpine AS base # Set the working directory RUN mkdir -p /usr/src WORKDIR /usr/src # Copy the application files COPY . /usr/src # Install dependencies and build the application RUN yarn EXPOSE 3000 CMD ["yarn", "run", "start"] ``` This Dockerfile sets up a container with Node.js, installs the necessary dependencies, builds the Next.js application, and starts the application on port 3000. Additionally, create a `.dockerignore` file to exclude specific files and directories from the build context. This helps to optimize the build process and reduce the size of the final Docker image. ``` node_modules .npm .git ``` Now, you're ready to create the `docker-compose.yml` file and the Nginx configuration in the parent directory to complete the setup. ### Nginx Configuration Next, you're going to want to create an Nginx configuration file. - Create a directory named `nginx` in the same folder where your `docker-compose.yml` file will be. - Create `nginx.conf` and add the following: ```bash server { server_name example.com www.example.com; # for certbot renewal location ~ /.well-known/acme-challenge { allow all; root /data/letsencrypt; } location / { # Port that next js is running on proxy_pass http://nextjs:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; } listen 443 ssl; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; } server { if ($host = example.com) { return 301 https://$host$request_uri; } # managed by Certbot server_name example.com www.example.com; listen 80; return 404; # managed by Certbot } ``` ### Docker Compose Create a `docker-compose` file to define the Next.js and Nginx services: ```yaml version: '3.8' services: nginx: container_name: 'nginx' image: nginx:latest ports: - "80:80" - "443:443" volumes: - ./nginx/nginx-conf:/etc/nginx/conf.d/default.conf - /etc/letsencrypt/ssl-dhparams.pem:/etc/letsencrypt/ssl-dhparams.pem - /etc/letsencrypt/options-ssl-nginx.conf:/etc/letsencrypt/options-ssl-nginx.conf - /etc/letsencrypt/live/example.com/fullchain.pem:/etc/letsencrypt/live/example.com/fullchain.pem - /etc/letsencrypt/live/example.com/privkey.pem:/etc/letsencrypt/live/example.com/privkey.pem networks: - docker-network restart: always nextjs: build: ./nextjs-app ports: - "3000:3000" restart: always networks: - docker-network networks: docker-network: driver: bridge ``` ## Deploying Your Application With the Docker and Nginx configuration in place, it's time to deploy the application. 1. Run `docker-compose up -d` in the same folder as your `docker-compose.yml` file. 1. Test the application by accessing the domain in a web browser. You should see your application served securely over HTTPS. ## Managing Memory One thing to keep in mind with this set up is the build cache. I've run into the scenario where the build cache on the machine reaches over 10GB. To solve this problem, you'll need to periodically remove the build cache: ```bash sudo docker builder prune ``` This should free up the disk space. Note however that this may result in a longer build time for your next release. ## Conclusion By following this tutorial, you have successfully containerized a Next.js application using Docker, deployed it with Nginx on Google Cloud Compute Engine, and secured it with Let's Encrypt SSL certificates. The deployment process outlined in this guide can be applied to other VPS providers, making it a versatile solution for any Next.js project. With this deployment strategy, you can focus on developing your application, knowing that your project is efficiently hosted and securely served to your users. Now that your site is deployed, you'll want search engines to discover it. See [Adding a Sitemap to Next.js](/posts/adding-a-sitemap-to-nextjs/) for a quick setup with automatic generation on every build. If you're using an AI coding agent like Claude Code to run deployments like this one, a misfired command can take down your server. See [Claude Code Hooks: PreToolUse & PostToolUse](/posts/claude-code-hooks-pretooluse-posttooluse/) for adding guardrails around the commands your agent executes. ## Adding a Sitemap to Nextjs Source: https://paul-schick.com/posts/adding-a-sitemap-to-nextjs/ Markdown: https://paul-schick.com/posts/adding-a-sitemap-to-nextjs/index.md Description: Learn how to add an XML sitemap and robots.txt to your Next.js site using next-sitemap. Quick setup with automatic generation on every build. ## Overview Sitemaps serve as a directory of the pages and content on your website, and creating these files is one of the most basic SEO steps to take. A sitemap is essentially a roadmap to the pages on your website. It can be in either HTML or XML format, with the latter used in the `next-sitemap` package that I'll discuss here. `robots.txt` communicates which pages your site permits bots to crawl. While this considered a terms of use agreement, it *does not* restrict actors from accessing your site. This file is not sufficient for ensuring operational security. Additional measures must be implemented to prevent unauthorized access to URL paths and files. The `robots.txt` file is a tool to assist search engines in crawling your site. Generating sitemaps and a `robots.txt` file is incredibly simple with [next-sitemap](https://github.com/iamvishnusankar/next-sitemap). Prioritizing efficiency, I used the most basic configuration for this package, which met my needs. As the blog grows, I may revisit some of the configuration options. I just needed these files, and I did not want to manually create them. It's also great that the `postbuild` script ensures that the files are updated whenever the application is built for production. This means that anytime I add a new post and rebuild, the sitemap will be updated with the new URL. ## Using `next-sitemap` By default, the build process generates an index sitemap along with other sitemaps containing website URLs. To generate a `robots.txt` file, you must add `generateRobotsTxt: true` as an option in the configuration file. The project's README contains information on splitting sitemap files when you have a large number of URLs (over 7000). That information can be found [in this section](https://github.com/iamvishnusankar/next-sitemap#index-sitemaps-optional). 1. Install the package using your package manager of choice: ```sh yarn add next-sitemap ``` 1. Next, you'll need to create a configuration file. This should go in the root directory of your Next.js project. The file must be called `next-sitemap.config.js`. ```js // next-sitemap.config.js /** @type {import('next-sitemap').IConfig} */ module.exports = { siteUrl: process.env.SITE_URL || 'https://paul-schick.com', generateRobotsTxt: true, } ``` 3. I added a `postbuild` script to my `package.json` to generate these files when the application is built for production. ```json // package.json scripts { "scripts": { "dev": "next", "build": "next build", "postbuild": "next-sitemap" } } ``` The command run by node is simply `next-sitemap`. I'll be covering more `postbuild` stuff in a later post while I document my Next.js blog strategy. I'm using static files rather than a database (for now), so I want to make use of some command line builds to reduce indexing time and generate static files. If you're looking for a deployment guide to go with this, see [Deploying a Next.js App with Docker and Nginx on GCP](/posts/deploying-nextjs-with-docker-nginx-on-gcp/) for a full walkthrough of containerization, reverse proxy setup, and SSL. And if you're building your Next.js site with an AI coding agent, [How to Write the Perfect CLAUDE.md File](/posts/how-to-write-claude-md/) covers giving the agent durable project context - including conventions like the sitemap and build scripts set up here.