Overview
I build small Windows desktop tools in Python, plus a couple of self-hosted web dashboards, so gauge for yourself which parts of this fit your own stack.
I direct, a small team of models does the work, and every irreversible step waits on me. The requirements are mine, because these are tools for work I actually do, and design choices come to me as options with tradeoffs that I pick between. What I don’t do is write, read, review, or verify the code, because I’m not a coder, so the layered reviews, the tests, and the approval gates all catch what I can’t catch by reading the code myself. A senior developer (Opus) turns my decisions into a plan and reviews the result; junior developers (cheaper models) write the code, and a QA reviewer that never sees the plan checks the finished change against what I originally asked for. I accept it only after running it myself on the real machine.
One thing shapes the rest: I run this natively on Windows, in the VS Code extension, with no sandbox. Claude Code’s sandbox doesn’t support native Windows, so there’s no operating-system boundary underneath any of this; the permission gates are the boundary. That’s why git commands, anything that reaches another machine, and anything destructive all stop and ask.
I also run Codex alongside Claude Code and split my work between them, so I do not hit a usage limit on either one mid-task. The Codex side has its own write-up, and nothing below depends on it. If you want to build this yourself, the build order is in “Copying this setup” at the end.
The team
I own the decisions, the approvals, the acceptance testing, and every git yes. The models below do the work.
| Role | Model and effort | Defined in | What it does |
|---|---|---|---|
| Senior developer | Opus 4.8, medium effort | ~/.claude/settings.json |
Turns my decisions into a plan, proposes designs with a recommendation, reviews the juniors’ work, runs git after my yes. Writes little code itself. |
| Junior developer | Sonnet 5, medium effort | ~/.claude/agents/plan-implementer.md |
Writes code to the approved plan and reports back. |
| Researcher | Sonnet 5, low effort | ~/.claude/agents/Explore.md |
Wide read-only searches, so the noise stays out of the senior’s context. |
| QA reviewer | Sonnet 5, high effort | ~/.claude/agents/plan-reviewer.md |
Checks a finished change against my original request, before a release or when I ask. |
What the senior does itself versus hands down is a question of certainty, not size. If it can already name the exact edits, it makes them, however many files that touches. If the work still has to be figured out, a junior does it.
The config files
| File | Where | What it does |
|---|---|---|
| Global rules | ~/.claude/CLAUDE.md |
The always-loaded rules file, holding everything I want followed in every session. |
| Skills | ~/.claude/skills/ |
My build conventions as instructions Claude loads on demand, one per project type or phase. |
| Review defaults | ~/.claude/code-review-defaults.md |
The settings my code-review skill reads when a review runs: effort levels, how many helper agents to fan out, how findings get reported. Kept out of the always-loaded rules file since it only matters during a review. |
| Output style | ~/.claude/output-styles/ |
How replies are written: answer first, plain language, everything waiting on me in one block at the end. |
| Settings | ~/.claude/settings.json |
Permission rules that turn the approval gates into prompts, and deny rules that keep secret files out of Claude’s file tools. |
| Hooks | ~/.claude/git-gate/, ~/.claude/hooks/ |
Scripts the harness runs on events: a command gate that catches gated commands the permission list would miss, plus desktop notifications when a session needs me. |
| Shared pipeline | Build-Tools repo |
The reusable release workflow every tool calls, plus the pinned dependency baseline they build from. |
How a project flows
- Research first. Does a good tool already exist? What’s the minimum useful version? Any complexity risks? Findings before code.
- Phased build. Plan, then one phase at a time: name and license, icon and splash, app, repo scaffolding. Each phase ends with a review and a stop.
- Smoke test. Before any exe build, the senior launches the app from source and I walk the changes: both themes, window persistence, bottom bar, one forced error, clean exit.
- Ship. Version bump, tag push (its own approval), CI builds on a clean runner, I click the release gate, concise notes after.
Habits that make it work
Branching before handing work to an agent, the approval gates that keep each step its own stop, and the two-review model for checking a diff each have their own page in the security section: delegation, enforcement, and the review model.
- Hold commits while testing. Nothing is settled until I say we’re good. A commit mid-test reads as “done” when I’m still finding problems.
- Work in phases, and not just for code. A fleet-wide rollout, a config overhaul, a research task: all planned as phases first. A phase boundary is a place the work can stop cleanly and move to a fresh chat.
- Fresh chats between phases. End a phase with a handoff summary (or a
status.mdin the repo), start the next in a new session. Long chats cost more and follow rules worse: every message re-sends the whole conversation, so cost grows with length, and the rules at the top compete with more content for the model’s attention. An output style helps with the attention half, because the harness re-injects it as the chat grows in a way a rules-file line never gets. - Four working files per project, none committed. The habit I’d copy
first. Every project gets
status.md(where it is now, and the next action),roadmap.md(ideas not in this build),needs-fixed.md(known bugs not yet fixed), anddesign-notes.md(durable technical reference). All four are gitignored. The first three exist on day one;design-notes.mdarrives once there’s real reference to keep. They describe the present, so when something ships or gets fixed the entry is deleted rather than marked done. A file that only grows stops being read. - Three layers of testing. New non-UI logic ships with pytest tests in
the same change, and the senior runs them plus ruff from the repo’s own
.venvduring development, on the same pinned versions CI uses. On a tag push, CI re-runs lint, an import check, and the tests on a clean runner, catching anything that only worked because of something installed on my desktop. Then I smoke-test the app by hand. No unit test sees a broken UI, and neither does CI. - Tests can’t be bent to pass. During a fix loop the junior may not edit test files. If a test looks wrong rather than the code, that comes back to me as a decision.
- Loud failures. A failed save or write must surface a visible error in the UI and leave a line in a log. Quietly reporting success and losing data is never acceptable, so error paths are designed loud from the start.
- Privacy first. Nothing I build collects, shares, or sells user data, and there’s no tracking in any of it.
- Rules live where they fire. Conventions in skills, project facts in the
repo’s CLAUDE.md, personal workflow in the global CLAUDE.md. One home per
rule. Because those three cover everything, I turn automatic memory off
(
autoMemoryEnabled: false) rather than let a fourth, invisible store build up. - Never combine the trifecta. Private data, untrusted outside content, and an outbound channel never share one piece of work. Any two is fine. All three is a stop.
Copying this setup
Prerequisites: VS Code 1.94 or later, the Claude Code extension, a paid
Claude subscription, git, and a GitHub account. No API key. The extension
bundles its own copy of the CLI for its chat panel, so you don’t need to
install the CLI separately; a standalone install only matters if you want to
type claude in a terminal, since the extension doesn’t put it on your PATH.
Everything below lives in ~/.claude/, which the extension and the CLI
share.
Build it in this order, since each piece leans on the ones before it. Everything is a plain text file, and steps 1 to 3 are a one-session job.
-
Global rules:
~/.claude/CLAUDE.md. The one file loaded into every session. It covers who you are and what you own versus delegate, the role split (you decide, the senior plans and reviews, a cheaper model types), the git approval gates, the file and remote-access rules, the trifecta rule, and how you want Claude to write to you. Keep it to the rules you always want followed, because this is the file whose length you pay for in every message.Two of mine, to show the shape:
**A prior yes never carries to the next step.** This includes tag pushes: pushing a version tag can trigger a CI build or release, so it is its own approval gate. Where a release pipeline has a deployment gate, that click is mine, not yours. **"Thoughts?" means discuss, not do.** When I ask for an opinion ("thoughts?", "should we...?", "I'm wondering if..."), give your read and a recommendation, then stop. Don't change code or files until I say go, even if the idea was mine. -
The junior developer:
~/.claude/agents/plan-implementer.md. A markdown file whose frontmatter names the agent, tells the senior when to launch it (thedescription), and pins it to a cheaper model and effort level (model: sonnet,effort: medium). The body is the standing orders: restate the plan before touching anything, follow the project’s existing conventions, never change git state or touch remote devices, never pull in a new third-party dependency without asking, stop and ask on ambiguity, and report back with every file changed and why. -
Settings:
~/.claude/settings.json. Two permission lists make the gates mechanical instead of honor-system.An
asklist forces a prompt on anything hard to undo. Mine covers four groups: git state (git commit,git merge,git push,git tag,git config,git remote), publishing (gh release,gh gist,gh pr merge,gh api,gh auth,gh secret,gh workflow run, and everygh repoverb that changes a repo:edit,create,delete,archive), anything that touches another machine (ssh,scp,sftp,rsync,ssh-copy-id), and destructive local commands:git reset,git checkout,git restore,git clean,git stash drop,git stash clear, plusrm,rmdir,dd,shred,truncateand the Windows equivalentsRemove-Item,del,Clear-Content,Format-Volume.Three of those are easy to talk yourself out of.
git checkoutlooks harmless, but holding commits while I test means hours of work can sit uncommitted, where a singlegit checkout .erases it with nothing to recover from.git configandgit remoteearn their place because both can quietly change where a push lands.A
denylist is the other half: it blocks Claude’s file-reading tools from opening.envfiles, private keys, certificates, and stored credentials.Both lists match the beginning of the command. So they see
rm -rf build/typed at the shell, and miss the identical deletion chained behind something else (cd build; rm -rf .) or buried inside a shell script or apython -cone-liner, which is just an allowed interpreter as far as the matcher is concerned. The chained command is fixable with a hook (step 4). The interpreter isn’t: closing that at the OS level takes the sandbox, which runs on macOS, Linux, and WSL2 but not native Windows. The written rules are the real guard; the lists and the hook are the backstop for when a rule gets forgotten.A starting point to paste and adapt:
{ "permissions": { "ask": [ "Bash(git commit:*)", "Bash(git merge:*)", "Bash(git push:*)", "Bash(git tag:*)", "Bash(git reset:*)", "Bash(git checkout:*)", "Bash(git restore:*)", "Bash(git clean:*)", "Bash(git config:*)", "Bash(git remote:*)", "Bash(gh release:*)", "Bash(gh gist:*)", "Bash(gh pr merge:*)", "Bash(gh repo edit:*)", "Bash(gh api:*)", "Bash(gh auth:*)", "Bash(gh secret:*)", "Bash(gh workflow run:*)", "Bash(ssh:*)", "Bash(scp:*)", "Bash(sftp:*)", "Bash(ssh-copy-id:*)", "Bash(rsync:*)", "Bash(rm:*)", "Bash(rmdir:*)", "Bash(dd:*)", "Bash(shred:*)", "Bash(truncate:*)", "Bash(mkfs:*)" ], "deny": [ "Read(**/.env)", "Read(**/.env.*)", "Read(**/*.pem)", "Read(**/id_rsa*)", "Read(**/.git-credentials)" ] } }Three things worth knowing. If you use more than one shell tool, every pattern needs duplicating for each, since
Bash(git push:*)doesn’t cover the same command run through PowerShell. A rule that names a flag (Bash(gh api -X:*)) is defeated by reordering the arguments, so gate the whole command. And the gates are only as complete as the list: a rule that says “ask before making a repo public” does nothing unlessgh repo editis actually on it. -
A command gate, if you run unsandboxed: a
PreToolUsehook. The lists in step 3 match the start of the command, socd build; rm -rf .never matchesrm. A hook closes that gap. Claude Code runs it before every shell call and hands it the command on stdin; the script prints a decision as JSON and exits. Mine splits the command on shell separators, drops wrappers likesudoand leadingVAR=xassignments, normalizes each segment’s first token sogit.exeand/usr/bin/gitboth read asgit, and returns “ask” if any segment starts a gated command.Three properties are worth keeping. It fails loud: unreadable input, malformed JSON, and unexpected errors all return “ask”, so silence only ever means a clean non-match. It’s standard library only, with no config and no state files, so it can be deleted without disturbing anything else. And it has its own tests, because a gate nobody tests is one you find out about the day it misses.
Notifications are the other good use of hooks and much simpler. Mine fire a desktop toast when a session stops, wants a permission, or asks a question, so I’m not watching a window think.
-
Occasional rules: a separate file the global CLAUDE.md points at. Some rules only matter during one kind of task, and those don’t belong in the file that loads on every message. Give each its own file and point at it with a single line in CLAUDE.md. Mine is
~/.claude/code-review-defaults.md: review effort levels, how many helper agents to fan out to, and how results get reported. It loads only when a review runs. -
Skills, as your conventions settle:
~/.claude/skills/<name>/SKILL.md. One folder per skill, one skill per project type or phase (mine: new project, theme and UI, test session, build and ship, repo setup, fleet rollout, server tools). Not every skill is a build phase; the drift audit above is one too, because it’s a task with a procedure. A shared name prefix keeps the family sorted together. Each SKILL.md starts with frontmatter, a name plus a description listing the trigger phrases. The description is what Claude reads when deciding whether to load the skill. The body is your conventions as step-by-step instructions.Two kinds of file keep that body from bloating.
templates/holds boilerplate meant to be copied into a project: license text, starter code, config files.references/holds detail meant to be read, not copied, and only when needed: the full release procedure, the exact UI wiring, per-file conventions. The skill body names the reference and says when to open it. My build-and-ship skill is about 120 lines pointing at four reference files; it used to be 329 with the detail inline, which meant any task touching shipping pulled in the whole release procedure to check one convention.Write down the conventions you already know on day one, and capture the rest the second time you find yourself explaining one in chat.
-
A shared pipeline, once you ship: a build-tools repo. One reusable release workflow every project calls (lint, import check, tests, clean build, a portable zip and installer with SHA-256 checksums, a provenance attestation so anyone can verify a download was built from the public source, a human-gated publish) plus the pinned dependency baseline every tool copies, so a fix lands in one place instead of one repo at a time.
One caveat: ~/.claude lives outside any project, so nothing backs it up by
default. Make it a git repo of its own with a private remote. Version
control also gets you history when a rule edit goes wrong. The directory
also holds session transcripts, caches, and other private state, so the
repo’s .gitignore should ignore everything by default (/*) and then
whitelist only the hand-authored config: the global CLAUDE.md, any reference
files it points at, settings.json, the skills, agents, and output-styles
folders, and any hook scripts settings.json names. That last one matters: if
settings.json is backed up but the scripts it points at are not, a restore
comes back referencing files that don’t exist.