Guides

Claude Code setup for Next.js

A good Next.js setup for Claude Code has three parts: a short CLAUDE.md with your real commands and server/client component rules, optional path-scoped rules for the parts of the app that differ, and hooks that run your linter and type checker so errors come back to Claude as feedback instead of surfacing in CI. This guide assumes the App Router, TypeScript and npm; swap in pnpm or yarn as needed.

1. Write a CLAUDE.md with commands and conventions

Put it at ./CLAUDE.md or ./.claude/CLAUDE.md. Run /init to generate a starting point, then trim it. Claude Code's docs recommend targeting under 200 lines per file, because longer files reduce adherence. See the CLAUDE.md template for a fuller structure.

# Project

## Commands
- Dev server: `npm run dev` (do not start it unless I ask; it is usually already running)
- Lint: `npx eslint .`   Format: `npx prettier --write <file>`
- Typecheck: `npx tsc --noEmit`
- Tests: `npm test`; one file: `npm test -- path/to/file.test.tsx`
- Build: `npm run build` (run before declaring a routing or config change done)

## Conventions (App Router, `app/`)
- Components are Server Components by default. Add `"use client"` only when the file
  needs state, effects, event handlers or browser APIs.
- Keep `"use client"` files small and push them to the leaves of the tree. Pass server-fetched
  data down as props instead of fetching in a client component.
- Fetch data in Server Components or route handlers, not with `useEffect`.
- Mutations go through Server Actions in `app/**/actions.ts` ("use server"); validate input
  with zod and never trust client-supplied ids.
- Never import server-only code (db client, secrets) into a client component.
- Env vars: only `NEXT_PUBLIC_*` may reach the browser. Never read `.env*` files.
- Use `next/image` and `next/link`, not raw `<img>` and `<a>` for internal routes.
## Don't
- Don't edit `package-lock.json` by hand or install packages without asking.
- Don't disable ESLint rules or add `any` / `@ts-ignore` to silence errors; fix the cause.

CLAUDE.md is context, not enforcement. Claude usually follows it, but anything that must happen every time belongs in a hook (step 3).

2. Scope extra rules with .claude/rules

If some conventions only matter for certain files, move them out of CLAUDE.md into .claude/rules/*.md with paths frontmatter. Per the docs, these load only when Claude reads, writes or edits a matching file, which saves context. Example .claude/rules/server-actions.md:

---
paths:
  - "app/**/actions.ts"
  - "app/api/**/*.ts"
---

# Server Actions and route handlers

- Start action files with "use server"; export only async functions.
- Validate every input with zod before touching the database.
- Check the session first; return typed errors, don't throw raw database errors.

3. Run ESLint, Prettier and tsc with hooks

A PostToolUse hook runs after Claude edits a file. Exit code 2 sends stderr back to Claude as feedback. A Stop hook can run the slower whole-project typecheck once, when Claude thinks it is done. Add both to .claude/settings.json (committed, so the team shares it):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh", "timeout": 60 }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/typecheck.sh", "timeout": 180 }
        ]
      }
    ]
  }
}

The per-edit script reads the JSON payload from stdin and takes tool_input.file_path. It formats, then lints only the edited file:

#!/usr/bin/env bash
# .claude/hooks/lint.sh   (chmod +x it)
f=$(python3 -c 'import json,sys;print(json.load(sys.stdin).get("tool_input",{}).get("file_path",""))')
case "$f" in *.ts|*.tsx|*.js|*.jsx|*.mjs) ;; *) exit 0 ;; esac
cd "$CLAUDE_PROJECT_DIR" || exit 0

npx --no-install prettier --write "$f" >/dev/null 2>&1
out=$(npx --no-install eslint "$f" 2>&1) || { echo "$out" >&2; exit 2; }
exit 0

The stop-time script checks stop_hook_active, which the docs say to do so a Stop hook doesn't keep re-triggering itself:

#!/usr/bin/env bash
# .claude/hooks/typecheck.sh   (chmod +x it)
input=$(cat)
[ "$(echo "$input" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("stop_hook_active",False))')" = "True" ] && exit 0
cd "$CLAUDE_PROJECT_DIR" || exit 0

out=$(npx --no-install tsc --noEmit 2>&1) || { echo "$out" | head -40 >&2; exit 2; }
exit 0

Test a hook without Claude first, then run /hooks to confirm it is registered:

echo '{"tool_input":{"file_path":"'"$PWD"'/app/page.tsx"}}' | .claude/hooks/lint.sh; echo "exit=$?"

You can also generate and test hook configs with the free hook builder. More patterns, such as blocking edits to lockfiles and .env files, are in 6 practical hook examples.

Common mistakes

MistakeFix
Claude adds "use client" to whole pagesState the default in CLAUDE.md and ask it to extract the interactive part into a small client component
Hook runs tsc on every edit and feels slowLint per file in PostToolUse; typecheck once in Stop
Claude starts a second npm run devTell it in CLAUDE.md that the dev server is already running
Hook errors with command not foundUse npx --no-install from the project root so local binaries are used
Hook path breaks after Claude cdsReference scripts via "$CLAUDE_PROJECT_DIR"
Stop hook loops foreverExit early when stop_hook_active is true, as in the script above

Where to go next

Skip the setup: get the tested versions

Keelwork bundles 10 workflow skills, 5 tested safety hooks (including a full guard-bash and a secret scanner), 3 subagents and 5 CLAUDE.md templates, with a one-command installer that safely merges into your settings.

Get Keelwork — $24 →