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.

One thing shapes the rest: Claude Code’s own sandbox doesn’t support native Windows, so the permission gates are all that stop a command. That’s why git commands, anything that reaches another machine, and anything destructive all stop and ask, and bulk deletes are refused outright. Everything runs inside a Windows 11 Pro virtual machine in Hyper-V on my desktop, holding VS Code with the Claude Code and Codex extensions plus both command-line tools, and no AI desktop apps. That doesn’t make any single command safer, but a mistake stays inside the virtual machine instead of reaching the desktop.

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.

Any of these worker roles can go to a Codex counterpart instead of a Claude sub-agent, for the same job run on the other provider; delegating to Codex covers how and when.

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/command-gate/, ~/.claude/hooks/ Scripts the harness runs on events: a command gate that refuses bulk deletes and catches gated commands the permission list would miss, plus desktop notifications when a session needs me.
Safe delete ~/.claude/safe-delete/ The only way Claude deletes a folder. It takes exact paths, checks each one on disk, and refuses anything outside its short list of allowed places.
Shared pipeline Build-Tools repo The reusable release workflow every tool calls, plus the pinned dependency baseline they build from.

How a project flows

  1. Research first. Does a good tool already exist? What’s the minimum useful version? Any complexity risks? Findings before code.
  2. 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.
  3. 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.
  4. Ship. Version bump, tag push (its own approval), CI builds on a clean runner, I click the release gate, concise notes after. When the installer or its bundled packages changed, a test release goes out first, and the senior installs the current public version in Windows Sandbox (a throwaway copy of Windows that erases itself on close), upgrades it with the test release, and confirms the user’s data survives the upgrade and uninstall before the real release.

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 safeguards 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.md in 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), and design-notes.md (durable technical reference). All four are gitignored. The first three exist on day one; design-notes.md arrives once there’s real reference to keep. They describe the present. A needs-fixed.md entry is marked [COMPLETED] once its fix is pushed and deleted once it ships; elsewhere a finished 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 .venv during 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 machine. 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.
  • Folders are deleted through one tool. Claude deletes a folder only by handing exact paths to a small script that checks each one on disk first. It removes only folders in Claude’s own session temp area and build or cache folders that git already ignores, and refuses everything else. Every other bulk delete is refused by the command gate (step 4 of “Copying this setup”).
  • PowerShell 7, not 5.1. Windows still ships Windows PowerShell 5.1, which silently ignores a filter that PowerShell 7 honors; that difference deleted a whole repository folder once. Before any command that deletes, moves, or overwrites files, Claude confirms the shell is version 7 or later, and stops if it is 5.1.
  • 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.
  • Tokens stay out of config files. An MCP server (a plug-in that gives Claude extra tools, such as access to my hosting account) that needs an API token reads it from a Windows user environment variable, so the config file, and the private repo that backs it up, never holds the secret. One catch: a chat only sees MCP servers that existed when it started, so start a new chat after adding one.
  • 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.

  1. 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.
  2. The junior developer: ~/.claude/agents/plan-implementer.md. A markdown file whose frontmatter names the agent, tells the senior when to launch it (the description), 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.

  3. Settings: ~/.claude/settings.json. Two permission lists make the gates mechanical instead of honor-system.

    An ask list 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 every gh repo verb 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, plus rm, rmdir, dd, shred, truncate and the Windows equivalents Remove-Item, del, Clear-Content, Format-Volume.

    Three of those are easy to talk yourself out of. git checkout looks harmless, but holding commits while I test means hours of work can sit uncommitted, where a single git checkout . erases it with nothing to recover from. git config and git remote earn their place because both can quietly change where a push lands.

    A deny list is the other half: it blocks Claude’s file-reading tools from opening .env files, 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 a python -c one-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 unless gh repo edit is actually on it.

  4. A command gate, if you run unsandboxed: a PreToolUse hook. The lists in step 3 match the start of the command, so cd build; rm -rf . never matches rm. 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 like sudo and leading VAR=x assignments, normalizes each segment’s first token so git.exe and /usr/bin/git both read as git, and returns “ask” if any segment starts a gated command.

    Bulk deletes get “deny” instead of “ask”: a recursive delete in any spelling (-Recurse, -r, rd /s), a delete fed paths through a pipe, a delete aimed at a variable, git clean, and delete calls written into the command text such as Python’s shutil.rmtree. The only exception is the safe-delete script run with exact paths, which applies its own checks. All the refusal checks run across the whole command before any “ask”, so a harmless first segment can’t let a later delete through on a single prompt. I learned to refuse rather than ask the hard way: this gate asked about a bulk delete that wiped a repository folder, and I approved it, because the command looked right. The prompt shows what a command says, not what it will do.

    Three properties are worth keeping. It fails loud: unreadable or malformed input is refused, an unexpected error refuses anything that looks like a delete and asks about everything else, and 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. It still reads only the command text, so a delete inside a script file gets past it; the written rule is the guard there.

    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.

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

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

  7. 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, any hook scripts settings.json names, and any script the rules tell Claude to run, such as safe-delete. Those last two matter: if the config is backed up but the scripts it points at are not, a restore comes back referencing files that don’t exist.