Charlotte, NC
ProjectsNovember 3, 2026

Fleet

Tap image to enlarge
Fleet is the private repository behind my development setup. It holds shell and git config, the global instructions every AI coding agent reads, agent skills, plugins, MCP servers and the settings I care about in Claude Code, Codex and Oh My Pi (OMP). Six machines run from it: an always-on Arch Linux command center, two Mac Studios and three MacBooks. I change the repo, push, and roll the change out from the command center with one script. A deploy ends with deploy: all ok or with the names of the machines that had problems. My machines had drifted apart. Before enrolling anything, I ran a read-only audit of each machine over SSH and wrote down what I found. The two Mac Studios were set up very differently. One had OMP, herdr, 13 Claude Code plugins and 95 skills. The other had Claude Code and Codex, no skills and no global agent instructions. One Mac's global gitignore listed AGENTS.md, so new agent instruction files were never committed from that machine. The homelab CLIs were installed differently on each machine, and some machines didn't have them. The agent config had drifted the most. On one Mac, OMP sent implementation subagents to GPT Sol. On the command center, OMP's slow role, which also handles planning, was Sol. Neither machine followed the rule I wanted, which is that Sol reviews code and doesn't write it. Each setting was a decision made once in one terminal and never carried anywhere else. I wanted every machine to have the same agents, instructions and tools, with each change made in one place. The repo lays files out by destination. agents/AGENTS.md is the global instruction file. skills/ holds skills. config/ holds files at their path relative to $HOME. inventory.yaml lists each machine with its OS, roles and checkout path, and agents read it to decide what goes where. Only the command center rolls changes out. It reaches the other machines over SSH on my Tailscale network. Each machine has its own checkout and pulls from GitHub with its own read-only deploy key, so a pull never waits on a 1Password or keychain prompt at the far end. install.sh links config into $HOME from three layers: config/shared/ for every machine, config/macos/ for the Macs, and config/machines/<host>/ for one machine. Each file becomes one symlink. When a more specific layer has the same path, its file replaces the less specific one whole. Nothing merges. I chose replacement because it's easy to predict. To know what a machine gets, I find the most specific copy of a file. The cost is that two machines that differ by one line need two full copies. So the repo rule is to put each file in the least specific layer that is correct for it, and to keep shared files free of hostnames, absolute home paths and Linux-only commands. install.sh never deletes a file it finds in the way. It moves the file into a timestamped backup folder, and restoring means copying it back. It also records the links it made, so when a file leaves the repo, the next run prunes its link. --dry-run prints what would change, and --check reports drift and exits 1. The script runs on the bash 3.2 that ships with macOS, and CI tests it there. Claude Code, Codex and OMP each look for global instructions in a different place. Fleet links the same agents/AGENTS.md to all three paths: ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md and ~/.omp/agent/AGENTS.md. Skills work the same way. Each skill lives once in skills/. install.sh links it into ~/.agents/skills, which OMP and Codex read, and into ~/.claude/skills for Claude Code. It links command-center skills only on machines with that role. The instruction file loads in every session of every tool, so the repo treats each line as a cost. A new line needs a reason, such as an agent that repeated a mistake or needed a fact it couldn't find by looking. A rule that must hold every time goes into a tool rule or hook instead. For example, git commands that discard work prompt for approval in Claude Code through permissions.ask and in Codex through a rules file. AGENTS.md states that rule in one line, and the tool rules enforce it. Claude Code and Codex rewrite their own settings files for plugin installs, hook installers, project trust and app state. If fleet linked those files, the tools would write into my checkout. So fleet doesn't link them. config/tool-settings.json lists the keys fleet owns, including the default model, the effort level, the git approval rules and commit attribution turned off. sync-tools.py sets exactly those keys and leaves the rest as the tool wrote it. With --check, it exits 1 if an owned key drifted. OMP's config shows why fleet doesn't link these files. Its config.yml started as a link into the checkout. Switching a model preset or changing a setting inside OMP then wrote into the repo, and the next deploy on that machine stopped on a dirty checkout. Now each machine has its own config.yml. Fleet owns every key in config/omp-settings.yml, and the next deploy resets an owned key that someone changed locally. sync-tools.py also handles MCP servers and plugins. OMP's mcp.json, built from the shared layer plus the machine layer, is the only list of MCP servers. The script adds each server to Claude Code and Codex and removes servers it added earlier that the list dropped. packages/plugins.json lists the plugins, and sync-tools.py has each tool install them from their marketplace with its own CLI. Each tool then updates them on its own. Day to day, I use five commands, all run on the command center: A real deploy starts with a gate. The commit on origin/main must have passed CI, and the script waits up to 10 minutes for a run that is still going. --force skips the gate for an emergency, and the output says it did. The script resolves that commit once and sends the same SHA to every machine, so a push that lands during a deploy never reaches the later machines. On each machine, the deploy fetches and fast-forwards to that commit, installs packages, and runs install.sh and sync-tools.py. Then it runs install.sh --check and sync-tools.py --check. The Macs install from a Brewfile, and the deploy trusts exactly the third-party formulae that file lists. The command center reports anything from its pacman list that isn't installed, and mise installs its pinned toolchains. If a machine's run stops before its end marker, the deploy counts that machine as failed, whatever the exit code. The dry run changes nothing on any machine. Every fleet file is a symlink into the machine's checkout, so moving the checkout to preview a commit would change live config. Instead, the dry run lists the incoming commits, then runs the install and sync previews from a temporary worktree at the deploy commit. Some steps need me at the machine. A Homebrew cask whose installer asks for a password can't get one over SSH. When those casks are the only failures, the deploy prints a manual: line with the command to run on that Mac and does not fail. fleet-doctor.py is the read-only side. It runs a doctor script on every machine in parallel and reports logins, tool versions, MCP handshakes, homelab CLI versions against the cluster, credential presence, checkout health and pending manual steps. It also compares OMP, Claude Code, Codex and herdr versions across machines and warns when they differ. Over SSH, a Mac's login keychain is locked, so the doctor marks Claude Code and gh logins as skipped instead of failed. I run it in a local terminal on the Mac for those two. A new Mac joins through five steps of scripts/enroll.sh, run from the command center. I read each step's output before I run the next one:
  1. audit runs the read-only audit over SSH, saves it, and scans it for token-like strings.
  2. key creates the machine's read-only deploy key, registers it on GitHub, clones the repo and records the checkout path in inventory.yaml.
  3. migrate runs the one-time migrations, as a dry run first.
  4. deploy runs a dry-run deploy, then the real one.
  5. check runs the doctor.
Migrations live in scripts/migrations/. Each one is a dry run unless I pass --apply, moves files instead of deleting them, and is safe to rerun. One of them retired a shared-config layer I had built by hand on one Mac to run two accounts per tool, which OMP now handles natively. The shell files differed on every Mac, so I clean up .zshrc, .zshenv and ~/.gitconfig by hand, working from the audit. I enrolled all six machines between October 27 and 29, 2026. The first machines went through these steps by hand before enroll.sh existed. The last MacBook was the first machine to go through the script. Nine skills come from other people's repositories. Council, sol-loop and babysit-pr come from tstack. Unslop, technical-writing and blast-radius come from pstack in cursor/plugins. Grilling, to-spec and to-tickets come from mattpocock/skills. Each one keeps its license and an upstream.json that records the source repository, the revision it was last synced from, and the files taken from it. scripts/skills-update.py runs a three-way merge on each file with git merge-file. The base is the file at the recorded revision. Ours is fleet's copy. Theirs is the file at upstream's latest commit. Fleet's edits survive an update. An edit that collides with an upstream change shows up as a conflict with markers, and the script exits 1. CI also fails if conflict markers reach a skill. To keep merges clean, fleet's edits stay small, limited to frontmatter and a ## Fleet notes section at the end. Workflow changes go in the skill that calls the vendored one. Plugins work differently. Fleet never copies a plugin's skills into the repo. It records the plugin and lets each tool install and update it from the marketplace. Tokens, API keys, Claude Code and Codex auth files, gh's hosts.yml, databases, sessions and agent memories never enter the repo. The repo records which credentials a machine needs, never their values. The doctor checks only that each credential is present and never reads a value. A config that needs a secret uses an op:// reference, and agents read it from the 1Password CLI at run time. CI runs gitleaks over the full git history on every push. scripts/test-all.sh runs every check, and CI runs exactly that script on Linux. The checks cover bash and Python syntax, shellcheck, actionlint, install.sh in a throwaway home directory for every machine, the deploy steps, the owned settings, the vault scripts, skill updates, a config structure test and the secret scan. A second CI job runs install.sh under macOS's bash 3.2. CI downloads actionlint and gitleaks at pinned versions and checks each download's SHA-256. The structure test exists because of a bug that passed every other check. A regex matched across lines, and OMP's daily model preset ended up holding an indented copy of the whole OMP config. The file still parsed, and a role-by-role check still passed. Now the test compares each config file's structure with what fleet intends, not just the one value that changed. Each project I work on can have a project vault, an Obsidian work journal in its own private GitHub repo. Fleet doesn't contain any vault. It ships the skills that work with them. project-vault reads a vault at session start and writes state and a journal entry at session end. vault-sweep checks a vault against primary sources such as PR state. vault-workspace-setup creates a new workspace and vault from a template. Because these are fleet skills and not files inside each vault, every tool on every machine gets the same version. The shared MCP list also includes the Obsidian Vault RAG service for searching vaults. I wrote about the workflow behind it in Using Obsidian as a shared work brain for AI coding agents. Fleet's OMP config states its routing rule at the top: "every role or path that can edit files resolves to Claude. GPT Sol and Muse only review." Opus 5.5 is the default model and runs subagent tasks. A deep preset moves the default, slow and planning roles to Mythos 5.1 when I want it for a hard problem. GPT 6.1 Sol runs as the advisor and the reviewer. When an editing role has to fall back, it falls back to Sonnet 5.5, so Sol never takes over a coding session. The rule applies to OMP's routing. When I run Codex directly, Codex writes code with Sol. The third model is Muse. Muse Wonder Full 1.0 is Meta's model, and fleet runs it as OMP's muse agent. That agent gets four tools: read, grep, glob and bash. It can run a project's build and tests to check a claim, and its instructions forbid edits, commits and pushes. It couldn't be an author anyway. When I tested it with OMP's full default toolset, Muse's API rejected the request, so it only works as a reviewer with a small toolset. Its instructions tell it that it was brought in because it runs on a different model family than the author, and that its "value is disagreement, not confirmation." Muse is the third member of my council, next to Opus and Sol. Like Sol, it has an empty fallback chain, so if Muse is down, the review reports that instead of quietly swapping in another model. I've been really impressed with it as a reviewer. The skills I chain on top of this, from planning through review to an unmerged PR, need more than a paragraph, and I'm covering them in a separate post. For the terminal side of running several agents at once, see Managing AI coding agents across providers with Herdr. I enrolled all six machines between October 27 and 29, 2026. Every one of them now links the same AGENTS.md and the same skills, gets the same MCP servers, and has the same owned settings in Claude Code, Codex and OMP. The OMP model roles come from one file, so on every machine each OMP role that can edit files resolves to Claude, and Sol only reviews. If a role drifts on one machine, sync-tools.py --check reports it and the next deploy sets it back. A change is now one commit and one scripts/fleet-deploy.sh run from the command center. Every machine gets the commit that passed CI, and the run ends with deploy: all ok or with the names of the machines that had problems.
  • Bash, compatible with macOS bash 3.2, for install.sh, deploy and enrollment
  • Python 3 standard library for the tool sync, doctor and skill updates, with PyYAML only in tests
  • Git and GitHub with a read-only deploy key per machine
  • GitHub Actions for CI on Linux and macOS, with shellcheck, actionlint and gitleaks
  • SSH over Tailscale from the command center to every machine
  • Homebrew on the Macs, and pacman and mise on the Arch Linux command center
  • Claude Code, Codex, OMP and herdr as the tools fleet configures
  • 1Password CLI for op:// secret references
The fleet repository is private. It holds my machine inventory and personal configuration, so I'm not publishing it, and this page quotes it only in short excerpts. The vendored skills come from the public repositories linked above.

Related projects

Tracecheck

Evidence-backed code review for AI coding agents, with exact source references, independent Jev judgments, and explicit limits on what a review can prove.

Obsidian Vault RAG

Open-source retrieval for Obsidian vaults, with local search, a shared MCP service, project-scoped profiles, and source-checked citations for AI agents.

Rollo Label Printer

Personal label-printing tool that finds the real shipping label inside messy return PDFs and images, normalizes it to 4x6, and prints through direct IPP.

Garmin MCP Server

Open-source MCP server that exposes 34 Garmin Connect health and fitness tools to any AI assistant, with parallel fetching and Docker support.

blakemccarn.dev

Portfolio and blog built with Next.js 16, deployed to AWS via SST v4 with Cloudflare DNS, staging environments, and full IaC.

Charlotte Wire & Cable

Full-stack business website for a specialty wire distributor: searchable catalog, admin dashboard, and serverless AWS infrastructure via SST.

Paperless OCR Enhanced

Open-source Paperless-ngx companion service that uses LLM vision models to repair weak OCR and improve downstream document search.

Paperless Knowledge Graph

A knowledge graph over my Paperless-ngx archive that answers plain-language questions with citations and audits every claim against the original OCR.

RapidEPR

I founded RapidEPR, an AI tool that writes military evaluations and award citations for five services, and grew it from a personal project to 325,000 users.