What you will build
A project that is ready for Claude Code to work in safely. You will add a short CLAUDE.md that tells Claude your commands and rules, a shared settings file with permission rules, a personal settings file for your own overrides, a verification habit so "done" always comes with evidence, and hooks for the few rules that must never be skipped. You finish with a clear answer to "how much can Claude do on its own here, and what stops it?"
Everything below follows the official Claude Code docs at code.claude.com. Where this site does something specific, it says so.
Before you start
- A project in a Git repository, with commands that build, lint or test it. Create a new branch before you start.
- Claude Code installed. The docs give
curl -fsSL https://claude.ai/install.sh | bashfor macOS, Linux and WSL, plus Homebrew and WinGet options. Check withclaude --version. jqif you want the hook examples (brew install jqon macOS).- 45 to 60 minutes.
How it works
Claude Code reads context and obeys configuration, and those are different things. The docs are direct about it: CLAUDE.md files are "context, not enforced configuration". Claude reads them and tries to follow them. Permission rules, permission modes and hooks are enforced by Claude Code itself, whatever the model decides. A good setup puts each rule in the right layer.
- CLAUDE.md holds facts Claude needs every session: commands, layout, conventions, gotchas. It is advice.
- Rules and skills hold instructions that only matter sometimes. Path-scoped rules load when matching files are read; skills load when used.
- Settings files hold permission rules: what runs without asking, what always asks, what is denied.
- Permission modes set how much Claude may do before asking.
- Hooks run your scripts at fixed moments, for rules with zero exceptions.
- A verification habit makes Claude prove each change with a check it can run.
Step 1: Start Claude Code on a branch
Open a terminal in the project root, create a branch, then start Claude Code there. It loads CLAUDE.md files from the current folder and every folder above it, so the folder you start in matters.
git switch -c claude-setup
claudeFor your first session, start in plan mode so Claude reads and proposes without editing: press Shift+Tab until the status bar shows plan mode, or launch with claude --permission-mode plan. Ask it to explain the project to you. What it gets wrong is what your CLAUDE.md must say.
Check
git statusshows you are on the new branch.- Claude's summary of the project names your real build and test commands.
Step 2: Generate a CLAUDE.md, then cut it down
Run /init. Claude reads the codebase and writes a starting CLAUDE.md with the build commands, test instructions and conventions it finds. If one already exists, /init suggests improvements instead of overwriting it.
Then trim. The docs recommend under 200 lines per file, and give a test for every line: would removing this cause Claude to make mistakes? Keep commands Claude cannot guess, style rules that differ from defaults, test instructions, branch and PR rules, architecture decisions, required environment variables and non-obvious gotchas. Remove anything Claude can learn by reading the code, standard conventions, long tutorials and file-by-file descriptions. Write instructions concrete enough to check: "Run npm test before committing" rather than "test your changes".
# Commands
- Dev server: npm run dev (port 4310). Build: npm run build. Lint: npm run lint.
- Unit tests: node --test tests/
# Rules
- Client components must not import modules that use node:fs. Put labels and types in a separate file.
- Every UI change: capture light and dark at 390 and 1440 wide before saying done.
- Never deploy. Deploys are a human command.
# Gotchas
- three.js 0.185: set envMap on each material; envMapIntensity is ignored with only scene.environment.That is the shape, not a template to copy. Yours should hold your project's own commands and traps.
Check
- Run
/contextand confirm your file is listed under Memory files. - The file is under 200 lines, and every line is specific enough to verify.
Step 3: Split what does not belong in every session
Three places keep CLAUDE.md short:
CLAUDE.local.mdin the project root, for your personal notes about this project. Add it to.gitignore..claude/rules/*.md, one topic per file. A rule withpathsfrontmatter loads only when Claude reads matching files.- Skills in
.claude/skills/, for multi-step procedures. Their full text loads only when used.
A path-scoped rule looks like this:
---
paths:
- "src/app/api/**/*.ts"
---
# API rules
- Validate every request body before use.
- Return errors in the shared failure format.If you also use Codex or another agent that reads AGENTS.md, keep one shared file. Put your instructions in AGENTS.md and make CLAUDE.md a single line, @AGENTS.md, which imports it. That is exactly how this site's app folder is set up. Imports can nest up to four levels, and paths are relative to the file that contains the import.
Check
/contextlists the rules that apply to the files you are working on.- Your committed files contain no personal paths or secrets; those live in
CLAUDE.local.mdor your user settings.
Step 4: Write the shared settings and permissions
Claude Code reads settings from ~/.claude/settings.json (you, every project), .claude/settings.json (the project, commit it), .claude/settings.local.json (you, this project only) and managed settings from an organisation. When the same key is set twice, local beats project, and project beats user. Permission lists are merged across files rather than replaced.
Start the shared file with the commands you trust and the files Claude should never read:
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(npm run lint)",
"Bash(npm run build)",
"Bash(node --test *)"
],
"ask": [
"Bash(git push *)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)"
]
}
}Rules are checked in the order deny, then ask, then allow, and the first match wins. A deny can never be carved open by a more specific allow. In Bash rules, * matches any text, and a trailing * also matches the bare command. The docs warn to put the * after the subcommand: Bash(git log *) allows only git log, while Bash(git *) allows every git command.
The file is strict JSON: a comment or a trailing comma is a settings error on the next start.
Check
- Run
/statusand look for the settings sources line; your project file should be listed. - Run
/permissionsand confirm each rule shows with its source file. - Ask Claude to print
.env. The read is refused.
Step 5: Choose a permission mode for the job
The mode sets how much Claude may do before asking. From the docs:
default(shown as Manual): asks on first use of each tool.acceptEdits: approves file edits and common file commands inside the working folder.plan: reads and runs read-only commands, does not edit source.auto: a separate classifier reviews actions instead of you, blocking risky ones.dontAsk: denies anything that would prompt; reads and pre-approved tools still run.bypassPermissions: skips prompts. The docs say to use it only in isolated containers or VMs.
Use plan mode to explore, Manual or acceptEdits for everyday work you watch, and auto for longer tasks where you trust the permission rules. Set a project default with permissions.defaultMode in a settings file, or switch per session with Shift+Tab.
Check
- You can name the mode your session is in from the status bar.
- Nobody on the project uses
bypassPermissionsoutside a container.
Step 6: Make "done" mean "checked"
Claude stops when the work looks done. The docs' first best practice is to give it a check it can run: tests, a build, a linter, or a screenshot to compare. Then ask for the evidence, not a claim. Put this at the end of task prompts, or in CLAUDE.md:
Done means: npm run lint and npm run build pass, affected tests pass, and for UI changes you captured light and dark at 390 and 1440.
Show me the commands you ran and their last lines of output. If you could not check something, say what and why.This is the habit behind every change to this site: each result note lists what changed, the raw check results and what remains unverified. The bundled /verify skill builds and runs your app to confirm a change against the running app, and /code-review reviews the current diff in a fresh subagent. For a second opinion on larger changes, the docs suggest a subagent that sees only the diff and your plan, told to report gaps that affect correctness rather than style.
Check
- Claude's final message quotes real command output.
- Anything unchecked is listed as unchecked, not left out.
Step 7: Add hooks for rules with no exceptions
Hooks are shell commands Claude Code runs at fixed moments. A PreToolUse hook runs before a tool call and can block it. This one, from the docs, stops edits to protected files. Save it as .claude/hooks/protect-files.sh and make it executable with chmod +x:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
for pattern in ".env" "package-lock.json" ".git/"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0Register it in .claude/settings.json under hooks, event PreToolUse, matcher Edit|Write, with the command "$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh.
A Stop hook can act as a gate: if your check fails, it exits 2 and Claude receives the stderr text and keeps working. The docs say Claude Code overrides a Stop hook after eight blocks in a row, and that the script should exit early when the stop_hook_active input is true. A sketch, which we have not wired on this site:
#!/bin/bash
INPUT=$(cat)
[ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ] && exit 0
git diff --quiet && git diff --cached --quiet && exit 0 # nothing changed
OUT=$(npm run lint 2>&1) || { echo "Lint failed. Fix before finishing:" >&2; echo "$OUT" | tail -n 30 >&2; exit 2; }
exit 0Check
- Run
/hooksand confirm each hook is listed with its source file. - Ask Claude to add a comment to
.env. The edit is blocked with your message.
Step 8: Keep autonomy reversible
- Work on a branch, and let Claude commit there. Keep
git pushon ask. - Use checkpoints: press
Esctwice or run/rewindto restore code and conversation to an earlier prompt. They only track edits made through Claude's file tools, not Bash commands, so they do not replace git. - Run
/clearbetween unrelated tasks. After two failed corrections on the same issue, clear and write a better prompt. - Keep side effects manual. A deploy skill should set
disable-model-invocation: trueso only you can run it.
Check
- You have rewound one change on purpose and seen it restored.
- Nothing in your settings lets Claude deploy or push without asking.
Gotchas we hit
- A framework edited our agent file. In Next.js 16,
next devadds a managed block toAGENTS.mdwhen it detects an AI coding agent, and re-adds it if you remove it. Commit the block with your work so it stops showing up as a stray diff. - Rules in CLAUDE.md are advice. "Never deploy" in a markdown file is not a lock. Deny or ask rules, and hooks, are what enforce it.
- A client component imported a file loader. The first rule in the Step 2 example comes from a real 500 on this site: a client component imported a module that used
node:fs. Splitting labels and types into a Node-free file fixed it, and a one-line rule stops it happening again. - Evidence beats a summary. A status line saying "verified" is not proof. Our result notes quote commands and output so anyone can re-run them.
Take it further
- Read Write a Claude Code skill for a job you repeat to turn your check routine into a one-command skill.
- Read AI-assisted visual QA for the capture script behind the "390 and 1440, light and dark" rule.
- Try
/sandboxfor OS-level isolation of Bash commands, described in the docs' sandboxing page.
Quick checklist
- Work happens on a branch, never straight on main.
CLAUDE.mdis under 200 lines of specific, checkable facts;/contextshows it loaded.- Personal notes live in
CLAUDE.local.mdor user settings, not the repo. .claude/settings.jsonallows your safe commands, asks for pushes and denies secrets./statusand/permissionsshow the rules you expect.- You chose a permission mode on purpose;
bypassPermissionsonly in containers. - Every task ends with command output, not a claim.
- Hooks block edits to protected files;
/hookslists them. - You know how to
/rewind, and you still commit.