S
Claude Code logoClaude CodeFull-stack TypeScript

Setuproll Full-Stack Claude Rig

Setuproll editorial@setuproll
96.0Overall score

A complete Claude Code setup for shipping a full-stack TypeScript app: an Opus planner and reviewer wrap a Sonnet implementer, Postgres and Playwright MCP let it verify both data and UI, and four hooks keep autonomous sessions safe and green. Documented end to end in a deep walkthrough so you can copy the whole thing.

96.0Score
6Components

Install this build

Export
terminal
npx setuproll add setuproll-fullstack-claude-rig

Components

Model

  • Claude Opus 4.8 (plan + review)
  • Claude Sonnet 4.6 (implement)

MCP servers

  • github
  • filesystem
  • postgres
  • playwright
  • sentry
  • context7

Subagents

  • planner
  • implementer
  • reviewer
  • tester
  • db-migrator

Hooks

  • PreToolUse: secret-scan (block on match)
  • PostToolUse: prettier + eslint --fix
  • PostToolUse: typecheck changed files
  • Stop: run affected tests

Rules

  • Plan before editing more than 3 files
  • Never commit with failing tests or types
  • Reuse existing utils before adding a dependency
  • One reviewable commit per logical change
  • Read the relevant code before proposing an edit

Slash commands

  • /plan
  • /ship
  • /review-pr
  • /migrate
Setup

How we built this Claude Code setup and how to actually work with it

Our reference Claude Code rig for a full-stack TypeScript app: the exact CLAUDE.md, settings.json, .mcp.json, subagents, hooks and slash commands, plus the day-to-day loop to run them through. Steal the whole thing.

Setuproll editorial16 min read2026-06-19

Most people start out treating Claude Code like a faster autocomplete: open the terminal, describe a feature, accept the diff, fix whatever it broke. That is quicker than typing, but only a little, because the saved time goes into cleaning up hallucinated imports, untested handlers and edits that quietly violate the project's own conventions.

The thing that changes everything is realizing the model was never the bottleneck. The bottleneck is context and guardrails. Claude does not know your commands, your architecture rules, or which files matter, unless you tell it once and let it read that every turn. It does not check its own work unless you wire something to make it. Build that scaffolding and the same model goes from a clever intern to something you can hand a whole feature and mostly watch.

This is the entire setup, copy-paste honest. We assembled and ran it against a reference full-stack TypeScript project, and every config below is the real file. We 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 to run them through on a normal day.

What you need to follow along
Claude Code installed and authenticated, a repo you control, and ten minutes. Nothing here is paid or private. The MCP servers are public packages, and the hooks are plain shell scripts.

The project and where the config lives

The reference app, Orbit, 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.

project layout (config files only)
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    # 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/ui

The 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. Ours 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 you expect. Do not put essays in here; put the things it would otherwise get wrong.

CLAUDE.md
# 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.
Keep CLAUDE.md lean
Every line here is read on every turn, so bloat costs tokens and dilutes attention. Prune it monthly. If Claude keeps making the same mistake, that is a signal to add one precise line, not a paragraph. If a line never seems to change behavior, delete it.

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 (permissions), set a bit of environment, and register hooks. The permission rules use matcher strings like Bash(pnpm test:*) and Edit(apps/**), so you can pre-approve the safe, repetitive things and force a prompt only on the genuinely risky ones like pushing or migrating.

.claude/settings.json
{
  "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" }]
      }
    ]
  }
}
  • allow covers everything you never want to be interrupted for: reads, edits inside the app and package folders, and the read-only or idempotent commands (test, typecheck, lint).
  • ask is the small set of actions with real consequences. Pushing to git and running a real migration always stop and wait for a human.
  • deny is 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.
defaultMode is a knob, not a default
This config runs with acceptEdits so file edits apply without a prompt, but bash and the ask/deny lists still gate the dangerous parts. Skip the fully bypassing modes on a repo you care about; the few seconds saved are not worth an unsupervised git push.

.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 your 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.

.mcp.json
{
  "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 github are the baseline: scoped file access and read/write to issues and pull requests so you 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 context7 round it out: real production errors to triage, and up-to-date docs so it stops citing an API that changed two versions ago.
Add MCP servers one at a time
Every server adds tool definitions to the context window. Add them gradually and drop the ones you never use. Six is a sensible ceiling. If a server's tools never get called over a week, it is just tax.
punkpeye/awesome-mcp-serversThe curated list I pull new MCP servers from. Production-ready and experimental servers for files, databases, browsers and APIs.github.com60k+The Best MCP Servers for Developers in 2026Builder.io's roundup with setup notes for GitHub, Context7, Figma, Linear and more. Good for deciding what is worth the context cost.builder.io
Claude Code Best Practices - The Ultimate Guide26:11
Claude Code Best Practices - The Ultimate Guide· IndyDevDan

Subagents: 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 the reviewer reads twenty files to check a diff, all that noise stays in the reviewer's context, and only its short verdict comes back. 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. You 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.
  • 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.

.claude/agents/reviewer.md
---
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.
Match the model to the job
Planning and reviewing are reasoning-heavy, so they get Opus. Implementing an already-approved plan is mostly mechanical, so it gets Sonnet. This split is where most of the cost savings come from without losing quality on the decisions that matter.
Create custom subagents - Claude Code DocsThe official reference for the agent file format: frontmatter fields, tool allowlists and isolated context windows.code.claude.com
How to Build Claude Subagents Better Than 99% of People22:40
How to Build Claude Subagents Better Than 99% of People· IndyDevDan

Hooks: 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. Formatting and the secret scan should never 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.

.claude/hooks/secret-scan.sh
#!/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
Exit code 2 blocks, anything else does not
This is the part people get wrong. A PreToolUse hook only blocks the action on exit code 2. Exit 0 lets it proceed; any other non-zero is treated as a non-blocking error and the session continues. Test your blocking hooks before you trust them.
disler/claude-code-hooks-masteryReference repo covering every hook lifecycle event with prompt-level control and security examples. The exit-code contract is spelled out here.github.com3k+
Claude Code Hooks explained in 5 minutes5:42
Claude Code Hooks explained in 5 minutes· IndyDevDan

Slash commands: your repeated prompts, saved

Anything you type more than twice should become 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.

  • /plan hands the task to the planner subagent and waits for your approval.
  • /ship runs the full green-gate (typecheck, lint, changed tests) then stages and commits.
  • /review-pr pulls a PR via the github MCP and runs the reviewer over its diff.
  • /migrate routes to the db-migrator subagent for a safe, dry-run-first schema change.

This is /ship, the command that closes every unit of work. Because typecheck, lint and tests gate the commit in order, a red build stops before it ever reaches history.

.claude/commands/ship.md
---
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 to 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 you are never asking one overloaded context to do all four at once.

1. Explore and plan (read-only)

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. Expect to tweak it. Catching a wrong assumption here, before a single line is written, is worth more than any amount of fast coding later.

claude · orbit (planning)
$/plan add a soft-delete flag to projects and exclude them everywhere
→ planner subagent (opus), read-only
Read packages/db/schema/projects.ts, apps/api/projects/*, apps/web/projects/*
Plan:
1. Add deletedAt timestamp column to projects (packages/db)
2. Generate migration with pnpm db:generate (NOT applied yet)
3. Add notDeleted() helper + filter in the projects service
4. Update list/detail handlers to use it; soft-delete in DELETE route
5. Tests: deleted projects are hidden from list + return 404 on detail
Touches 6 files across packages/db and apps/api. Approve?
$approve, but also hide them from the web sidebar count
Updated plan: +1 step for apps/web/sidebar count query. Ready.
$

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 you see clean diffs, not a pile of unformatted code you have to mentally compile.

claude · orbit
You
go ahead with the approved plan
Agent
Step 1: added deletedAt to projects schema. Step 2: ran pnpm db:generate, migration 0042_soft_delete created (not applied).
Agent
Step 3: added notDeleted() to packages/db and filtered the projects service. [PostToolUse] prettier + eslint --fix applied. [PostToolUse] typecheck: clean.
Agent
Step 4: DELETE /projects/:id now sets deletedAt instead of removing the row; list + detail use notDeleted(). Ready for tests.
The implementer applying step 3, with the format + typecheck hooks firing after each edit.

3. Test and review before commit

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 you run /ship.

claude · orbit (review)
$/review-pr current diff
→ reviewer subagent (opus): reads diff, runs typecheck + affected tests
pnpm typecheck … clean
pnpm test --changed … 14 passed
Findings:
block apps/api/projects/list.ts:31 count query still includes deleted rows
warn packages/db/projects.ts:18 notDeleted() not exported from index
nit test name 'works' is vague
$fix the block + warn, leave the nit
implementer: count query now uses notDeleted(); helper exported. Re-review clean.
$
claude · orbit (ship)
$/ship feat(projects): soft-delete with global exclusion
pnpm typecheck … clean
pnpm lint … clean
pnpm test --changed … 15 passed
git diff --stat: 8 files changed, 142 insertions(+), 11 deletions(-)
Staged 8 files (no -A). Committed: feat(projects): soft-delete with global exclusion
[Stop hook] pnpm test --changed: green
$
PR #218 · Soft-delete projects
✓ feat(projects): soft-delete with global exclusion
✓ 8 files changed · 15 tests passing · types clean
⏸ pnpm db:migrate waiting for approval (ask permission)
→ db-migrator: dry-run shows 1 ALTER TABLE, 0 data loss. Apply? (y/N)
The migration runs through /migrate (dry-run first, then ask-gated apply), never automatically.

What this setup actually changes

We have not run a controlled before-and-after benchmark on this rig, so you will not find one here. What each piece is for is concrete enough: the planner catches wrong assumptions before code exists, the hooks make formatting, typechecking and secret scanning non-optional, the reviewer grounds its verdict in tests it ran itself, and the Opus/Sonnet split spends the expensive tokens only on the decisions that matter. The headline is not raw speed; it is that far less of what the model produces needs you to fix it.

The setup does not make the model smarter. It makes the model's output trustworthy enough that you stop re-reading every line, and that is the whole win.

Setuproll editorial

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.

zsh · your repo
$npx setuproll add setuproll-fullstack-claude-rig
✓ wrote CLAUDE.md, .mcp.json, .claude/settings.json
✓ wrote 5 subagents, 3 hooks, 4 slash commands
next: edit CLAUDE.md with your real commands, then run `claude` and try /plan
$
Do not copy it whole and walk away
This rig fits a pnpm + Next.js + Postgres monorepo. The shape transfers; the specifics do not. Swap the commands, drop MCP servers you will not use, and add one rule each time the model repeats a mistake. A setup you tuned beats a setup you inherited.
Claude Code Best PracticesAnthropic's canonical post on the explore/plan/code/commit loop, CLAUDE.md tuning and multi-agent patterns. The backbone of this whole approach.anthropic.comhesreallyhim/awesome-claude-codeThe curated list of skills, hooks, slash commands and subagents to raid for more ideas once the basics are in place.github.com23k+

0 Reviews

Your rating
Sign in to post

Loading discussion...