How I built my Claude Code setup and how I actually work with it
A year of tuning one Claude Code rig for a full-stack TypeScript SaaS: the exact CLAUDE.md, settings.json, .mcp.json, subagents, hooks and slash commands, plus the day-to-day loop I run them through. Steal the whole thing.
I ship a full-stack TypeScript SaaS on my own. For the first few months with Claude Code I treated it like a faster autocomplete: open the terminal, describe a feature, accept the diff, fix whatever it broke. It was quicker than typing, but only a little, because I spent the saved time cleaning up hallucinated imports, untested handlers and edits that quietly violated my own conventions.
The thing that changed everything was realizing the model was never the bottleneck. The bottleneck was context and guardrails. Claude does not know my commands, my architecture rules, or which files matter, unless I tell it once and let it read that every turn. It does not check its own work unless I wire something to make it. Once I built that scaffolding, the same model went from a clever intern to something I can hand a whole feature and mostly watch.
This is the entire setup, copy-paste honest. Every config below is what actually sits in my repo. I will walk through the project layout, the CLAUDE.md, the settings, the MCP servers, the subagents, the hooks, the slash commands, and then the loop I run them through on a normal day.
The project and where the config lives
The app is a pnpm monorepo: a Next.js 16 web app, a Fastify API, a Drizzle/Postgres package, and a shared UI package. Claude Code configuration is not scattered around. It all lives under one .claude directory plus two files at the repo root, so the whole setup is version-controlled and travels with the project.
orbit/
CLAUDE.md # project memory, read on every session
.mcp.json # MCP servers, shared with the team
.claude/
settings.json # permissions, env, hooks (committed)
settings.local.json # my personal overrides (gitignored)
agents/
planner.md
implementer.md
reviewer.md
tester.md
db-migrator.md
commands/
plan.md
ship.md
review-pr.md
migrate.md
hooks/
secret-scan.sh
format.sh
typecheck-changed.sh
apps/web apps/api packages/db packages/uiThe single highest-leverage file is CLAUDE.md. Claude Code loads it into context automatically at the start of every session, so it is the cheapest possible way to make every response better. Mine is deliberately short and specific. It tells Claude the real commands (so it stops inventing npm test when the repo uses pnpm test), the architecture boundaries it must not cross, and the workflow I expect. I do not put essays in here; I put the things it would otherwise get wrong.
# Project: Orbit (full-stack TypeScript SaaS)
## What this is
Orbit is a Next.js 16 (App Router) front end, a Fastify API, and Postgres
via Drizzle. Monorepo with pnpm workspaces: apps/web, apps/api, packages/db,
packages/ui. Deploys to Fly.io. Solo-maintained.
## Commands you should use (not invent)
- Install: `pnpm install`
- Dev (all): `pnpm dev` | web only: `pnpm --filter web dev`
- Typecheck: `pnpm typecheck` (runs tsc -b across the workspace)
- Lint/format: `pnpm lint` and `pnpm format`
- Unit tests: `pnpm test` | one file: `pnpm test path/to/file.test.ts`
- E2E: `pnpm e2e` (Playwright) | migrations: `pnpm db:migrate`
## Architecture rules
- The web app NEVER imports from apps/api. Shared types live in packages/db.
- All DB access goes through packages/db (Drizzle). No raw SQL in handlers.
- API routes validate input with zod at the boundary, then call a service.
- React: server components by default. "use client" only when it needs state.
## Conventions
- TypeScript strict. No `any`, no `as` casts to silence the compiler.
- Reuse helpers in packages/ui and packages/db before adding a dependency.
- Errors: throw typed AppError, never bare strings. Log via the logger, not console.
- Tests live next to the file as *.test.ts. Every new service needs one.
## Workflow expectations
- Read the files you are about to change before proposing edits.
- For anything touching more than ~3 files, write a short plan first and
wait for me to approve it.
- Keep commits small and reviewable: one logical change each.
- Never commit if `pnpm typecheck` or `pnpm test` is failing.
## Gotchas
- Drizzle migrations are generated, never hand-edited. Use `pnpm db:generate`.
- The Sentry DSN and DATABASE_URL come from env, never hardcode them.
- Fly secrets are managed separately; do not touch fly.toml without asking.settings.json: permissions, env and hooks
The .claude/settings.json file is what makes long, mostly-autonomous sessions safe to leave running. It has three jobs: decide which tool calls run without asking me (permissions), set a bit of environment, and register hooks. The permission rules use matcher strings like Bash(pnpm test:*) and Edit(apps/**), so I can pre-approve the safe, repetitive things and force a prompt only on the genuinely risky ones like pushing or migrating.
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Read(**)",
"Edit(apps/**)",
"Edit(packages/**)",
"Bash(pnpm test:*)",
"Bash(pnpm typecheck)",
"Bash(pnpm lint)",
"Bash(pnpm db:generate)",
"Bash(git status)",
"Bash(git diff:*)"
],
"ask": [
"Bash(git push:*)",
"Bash(pnpm db:migrate)"
],
"deny": [
"Read(.env)",
"Read(.env.*)",
"Bash(rm -rf:*)",
"Edit(fly.toml)"
]
},
"env": {
"NODE_ENV": "development",
"BASH_DEFAULT_TIMEOUT_MS": "120000"
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": ".claude/hooks/secret-scan.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/format.sh" },
{ "type": "command", "command": ".claude/hooks/typecheck-changed.sh" }
]
}
],
"Stop": [
{
"hooks": [{ "type": "command", "command": "pnpm test --silent --changed" }]
}
]
}
}allowcovers everything I never want to be interrupted for: reads, edits inside the app and package folders, and the read-only or idempotent commands (test, typecheck, lint).askis the small set of actions with real consequences. Pushing to git and running a real migration always stop and wait for me.denyis a hard wall. It can never read the env files, never run a recursive delete, and never touch the deploy config. Deny wins over allow.
.mcp.json: giving Claude real tools
MCP (the Model Context Protocol) is how Claude Code reaches outside the text in your repo. Without it, the model can read and edit files and run shell commands. With it, it can query my actual database schema, drive a real browser, read GitHub issues, pull live Sentry errors, and fetch current library docs. This is the difference between a model guessing what a table looks like and one that ran the query.
{
"mcpServers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
},
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/"
},
"postgres": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": { "DATABASE_URL": "${DATABASE_URL}" }
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@playwright/mcp@latest", "--browser", "chromium"]
},
"sentry": {
"type": "http",
"url": "https://mcp.sentry.dev/mcp"
},
"context7": {
"type": "http",
"url": "https://mcp.context7.com/mcp"
}
}
}- filesystem and
githubare the baseline: scoped file access and read/write to issues and pull requests so I can say "start on the bug in issue 214". - postgres lets the model inspect the live schema and run read queries before it writes a handler. It stops a whole class of "that column does not exist" errors.
- playwright gives it a browser. After a UI change it can actually click through the flow and confirm the thing renders, instead of declaring victory blind.
- sentry and
context7round it out: real production errors to triage, and up-to-date docs so it stops citing an API that changed two versions ago.
26:11Subagents: a small team instead of one overloaded model
A subagent is a separate Claude with its own system prompt, its own tool allowlist, and crucially its own context window. They live as Markdown files in .claude/agents/ with YAML frontmatter. The point is not to be fancy; it is to keep the main conversation clean. When my reviewer reads twenty files to check a diff, all that noise stays in the reviewer's context, and only its short verdict comes back to me. The main thread does not get buried.
- planner (Opus): reads the relevant code and writes a step-by-step plan with the files it will touch. No edits. I approve or correct the plan first.
- implementer (Sonnet): executes one approved plan step at a time. Fast and cheap, because the hard thinking already happened in planning and review.
- reviewer (Opus): reads the diff against the task and the rules, runs typecheck and affected tests, and reports concrete fixes. It never rewrites code itself.
- tester (Sonnet): writes and runs tests for new code, and chases down failures, reporting only the failing specs back to me.
- db-migrator (Opus): the only agent allowed near migrations. It generates a Drizzle migration, dry-runs it, and shows the SQL diff before anything applies.
Here is the actual reviewer definition. The frontmatter restricts its tools and pins it to Opus; the body is its system prompt.
---
name: reviewer
description: >
Reviews a diff for correctness bugs, missing tests and rule violations.
Use after the implementer finishes a change, before it is committed.
tools: Read, Grep, Glob, Bash
model: opus
---
You are a senior reviewer for the Orbit codebase. You receive a diff and the
task it was meant to accomplish. Your job is to find problems, not to praise.
Check, in priority order:
1. Correctness: does the code do what the task asked? Trace the data flow.
2. Rule compliance (see CLAUDE.md): no raw SQL in handlers, zod at the
boundary, no `any`, server components by default.
3. Tests: does every new service/handler have a test? Do they actually
assert behavior rather than just run?
4. Blast radius: any change that crosses a package boundary it should not.
Run `pnpm typecheck` and the affected tests yourself before judging.
Report findings as a short list: file:line, severity (block/warn/nit),
and the concrete fix. If it is clean, say so in one line. Do not rewrite
the code; hand the fixes back to the implementer.
22:40Hooks: automation that does not depend on the model remembering
Rules in CLAUDE.md are suggestions the model usually follows. Hooks are shell commands the harness runs deterministically at specific lifecycle events, whether or not the model feels like it. That distinction is everything for safety. I never want formatting or a secret scan to be optional, so they are hooks, not rules.
- PreToolUse on Bash runs a secret scanner that blocks any command that looks like it leaks a key or reads the env file. It can hard-stop the tool call before it ever runs.
- PostToolUse on edits runs prettier plus eslint --fix and then typechecks the changed files, so the model is always working against formatted, type-correct code.
- Stop runs the affected test suite when a turn ends, so a session never quietly finishes in a red state.
The hook reads the tool call as JSON on stdin and signals a block by exiting with code 2; its stderr is fed back to the model so it knows why it was stopped. Here is the real secret-scan hook.
#!/usr/bin/env bash
# .claude/hooks/secret-scan.sh
# PreToolUse hook on Bash. Reads the tool call as JSON on stdin.
# Exit 2 => block the command and show stderr back to the model.
set -euo pipefail
INPUT=$(cat)
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')
# Block obvious secret leaks and destructive resets.
if printf '%s' "$COMMAND" | grep -qiE '(api[_-]?key|secret|password|BEGIN [A-Z]+ PRIVATE KEY)'; then
echo "secret-scan: refusing command that looks like it handles a secret" >&2
exit 2
fi
# Never let it print the env file.
if printf '%s' "$COMMAND" | grep -qE 'cat[[:space:]]+.*\.env'; then
echo "secret-scan: reading .env is denied" >&2
exit 2
fi
exit 0
5:42Slash commands: my repeated prompts, saved
Anything I type more than twice becomes a slash command. They are Markdown files in .claude/commands/, and the body is just a prompt with placeholders: $ARGUMENTS for the whole argument string, $1 and friends for positional ones. The frontmatter can pin an allowed-tools list so the command only runs what it needs.
/planhands the task to the planner subagent and waits for my approval./shipruns the full green-gate (typecheck, lint, changed tests) then stages and commits./review-prpulls a PR via the github MCP and runs the reviewer over its diff./migrateroutes to the db-migrator subagent for a safe, dry-run-first schema change.
This is /ship. It is the command I run dozens of times a day, and it is why I have not committed a broken build in months.
---
description: Run the full green-gate, then stage and commit the change
argument-hint: "[commit message]"
allowed-tools: Bash(pnpm typecheck), Bash(pnpm lint), Bash(pnpm test:*), Bash(git add:*), Bash(git commit:*)
---
You are finishing a unit of work. Do this in order and stop at the first failure:
1. Run `pnpm typecheck`. If it fails, fix the types and rerun.
2. Run `pnpm lint`. Fix lint errors (not warnings) and rerun.
3. Run `pnpm test --changed`. All must pass.
4. Show me `git diff --stat`, then stage the relevant files and commit
with this message: $ARGUMENTS
Never use `git add -A`. Only stage files you actually changed for this task.How I actually work with it, step by step
All of that config exists to support one loop, and it is the same loop Anthropic recommends: explore, plan, code, commit. The trick is that each phase has the right tool and the right model behind it, so I am never asking one overloaded context to do all four at once.
1. Explore and plan (read-only)
I start every non-trivial task with /plan. The planner reads the relevant files and the issue, then comes back with a numbered plan and the exact files it intends to touch. I almost always tweak it. Catching a wrong assumption here, before a single line is written, is worth more than any amount of fast coding later.
2. Implement one step at a time
Once the plan is approved, the implementer works through it. Because the plan already decided the hard parts, this is the cheap, fast phase on Sonnet. The PostToolUse hooks format and typecheck each edit as it lands, so I see clean diffs, not a pile of unformatted code I have to mentally compile.
3. Test and review before commit
I do not let it commit on its own judgment. The tester writes the specs the plan called for, and then the reviewer reads the whole diff against the task and the rules. The reviewer runs typecheck and the affected tests itself, so its verdict is grounded, not vibes. Only when it comes back clean do I run /ship.
Was it worth it? Before and after
I tracked a month of feature work before this setup and a comparable month after. The headline is not raw speed; it is that far less of what the model produced needed me to fix it, which is what actually saves time.
| Metric | Ad-hoc (model only) | This build |
|---|---|---|
| Median feature, idea to merged PR | ~3.5 hrs | ~1.4 hrs |
| Diffs needing manual rework | ~55% | ~12% |
| Broken builds committed per week | 3 to 4 | 0 |
| Avg model cost per feature | $0.90 (all Opus) | $0.48 (split) |
| Tests written without me asking | rarely | every service |
The setup did not make the model smarter. It made the model's output trustworthy enough that I stopped re-reading every line, and that is the whole win.
Steal this setup
Everything above is in this build. Adding it drops the CLAUDE.md, settings.json, .mcp.json, the five subagents, the three hooks and the four slash commands into your repo, where you can trim them to your stack. Start by editing CLAUDE.md to your real commands and architecture, then keep the hooks; those two changes alone get you most of the value.