Fleet: Managing Claude Code Configs at Scale
Copy-pasting .claude/ directories doesn't scale. Fleet keeps 19 projects in sync with manifest-based config management and automatic conflict detection.
I run 19 projects on Claude Code. Every one of them has a .claude/ directory with agents, hooks, settings, and skills. For months, the way I kept these consistent was the most sophisticated configuration management technique known to software engineering: copy and paste.
It worked until it didn't. I'd fix a hook in one project, forget to update the others, and spend twenty minutes debugging a problem I'd already solved. Or worse, I'd update a shared agent definition, push it to five projects, and accidentally overwrite project-specific customizations that had taken hours to dial in.
The breaking point came when I realized I had three different versions of the same quality-gate agent across projects โ each with subtle differences I couldn't remember the reason for. Some were intentional customizations. Some were accidental divergence. I had no way to tell which was which.
That's when I built Fleet.
What Fleet Actually Is
Fleet is a cross-project configuration management system for Claude Code. It lives in ~/.claude/fleet/ and manages the .claude/ directories across every project I work on. Nine Bash scripts, 3,428 lines of code, 161 tests. No external dependencies beyond Python 3 (which macOS includes) and standard Unix tools.
The core idea is simple: instead of every project owning its own configuration independently, configurations are centrally stored in the fleet directory and deployed to projects through a sync process that understands the difference between "I updated this in fleet" and "I customized this locally."
That distinction โ which sounds trivial and is not โ is the entire reason Fleet exists.
The Three-Layer Architecture
Fleet uses a three-layer model. Understanding the layers explains most of the design decisions.
Layer 1: User Global (~/.claude/)
Skills and commands live here. This is a Claude Code design feature, not something Fleet invented โ anything in ~/.claude/skills/ and ~/.claude/commands/ is automatically visible to every project. No deployment needed. No syncing. Claude Code just sees them.
This was the first key insight that simplified Fleet enormously. I originally planned to manage skills and commands as fleet-deployable resources. Then I realized they were already global. Removing them from Fleet's scope cut the system's complexity in half.
Layer 2: Shared Common (fleet/common/)
Hooks, notification scripts, sounds, and base settings that apply to every project. The common layer includes:
fleet/common/
โโโ hooks/
โ โโโ pre-completion-check.sh
โ โโโ session-context.sh
โ โโโ edit-counter.sh
โโโ sounds/
โ โโโ attention.mp3
โ โโโ complete.mp3
โ โโโ error.mp3
โ โโโ progress.mp3
โ โโโ subagent.mp3
โโโ hooks.json
โโโ notify.sh
โโโ settings.base.json
Everything in fleet/common/ gets deployed to every active project. If I add a new sound or update a hook, one fleet-sync.sh --all pushes it everywhere.
Layer 3: Project Overrides (fleet/projects/PROJECT/)
Per-project agents, hooks, settings overrides, and agent variable definitions. This is where customization lives.
fleet/projects/MyWritingTwin/
โโโ agents/
โ โโโ quality-gate.md
โ โโโ seo-monitor.md
โ โโโ content-pipeline.md
โ โโโ daily-briefing.md
โ โโโ user-lifecycle.md
โ โโโ aeo-infra.md
โโโ hooks/
โ โโโ check-translations.sh
โ โโโ sync-translations.sh
โ โโโ post-test-audit.sh
โ โโโ post-blog-publish.sh
โโโ settings.project.json
โโโ agent-vars.json
The merge rule is deterministic: common -> project -> deployed. Project files override common files at the same relative path. Settings are deep-merged with special semantics (more on that below). Agent templates are rendered with project-specific variables.
Why SHA256 Manifests, Not Timestamps
The first version of Fleet was a wrapper around rsync. It worked exactly once. The problem: rsync uses modification timestamps, and timestamps lie.
Copy a file, and the timestamp changes. Open it in an editor and save without changing anything, and the timestamp changes. Clone a repo, and every file gets a new timestamp. Move a file between machines, and timestamps shift. Nothing about a timestamp tells you whether the content actually changed.
Fleet uses SHA256 checksums instead. Every time Fleet syncs a file to a project, it records the file's hash in a manifest:
{
"synced_at": "2026-02-28T14:32:01Z",
"files": {
"hooks/session-context.sh": {
"sha256": "a3f2c8d1e5b7...",
"source": "common"
},
"agents/quality-gate.md": {
"sha256": "7b9e4f2a1c8d...",
"source": "template"
}
}
}
This enables 3-way comparison: manifest checksum vs. current fleet source vs. current project file. That three-way comparison is what makes intelligent conflict detection possible.
6-State Conflict Detection
Every file, before deployment, gets classified into one of six states:
| State | Meaning | Action |
|---|---|---|
| SYNCED | File unchanged since last sync, fleet unchanged | Skip (no-op) |
| STALE | Fleet updated, project copy unchanged | Deploy (safe update) |
| MODIFIED | User customized locally, fleet unchanged | Skip (preserve work) |
| CONFLICT | Both user and fleet changed | Skip and warn |
| NEW | File doesn't exist in project yet | Deploy |
| MISSING | Was deployed, now deleted from project | Log warning |
The default behavior is conservative. MODIFIED and CONFLICT never overwrite without --force. This is the single most important design decision in Fleet. The system assumes your local work is more valuable than consistency โ because usually, it is. If I edited a hook to fix a project-specific issue, I don't want a fleet sync to blow that away.
Here's what it looks like in practice. Running fleet-status.sh MyWritingTwin:
MyWritingTwin (11 files tracked)
SYNCED hooks/session-context.sh (common)
SYNCED hooks/pre-completion-check.sh (common)
SYNCED hooks/edit-counter.sh (common)
SYNCED notify.sh (common)
SYNCED sounds/complete.mp3 (common)
SYNCED sounds/error.mp3 (common)
MODIFIED hooks/check-translations.sh (project)
SYNCED settings.json (merged)
STALE hooks.json (common)
SYNCED agents/quality-gate.md (template)
SYNCED agents/seo-monitor.md (template)
That MODIFIED status on check-translations.sh means I edited it directly in the project after the last sync. Fleet will preserve it. The STALE status on hooks.json means the fleet version was updated but the project copy hasn't been synced yet โ safe to push.
Agent Templates With Variable Substitution
This is the feature that made Fleet worth the investment. I have 7 agent templates โ quality-gate, seo-monitor, daily-briefing, content-pipeline, product-bootstrap, user-lifecycle, and aeo-infra. The quality-gate agent, for instance, is deployed to both MyWritingTwin and FluxDiagram. Same validation logic. Different project details.
The template looks like this:
---
name: quality-gate
description: Pre-deploy validation for {{PRODUCT_NAME}}
allowed-tools: Read, Bash, Grep, Glob, Write, Edit
---
# Quality Gate Agent
You are the Quality Gate agent for the {{PRODUCT_NAME}} project.
## Configuration
| Key | Value |
|-----|-------|
| PRODUCT_NAME | {{PRODUCT_NAME}} |
| PROJECT_ROOT | {{PROJECT_ROOT}} |
| TYPE_CHECK_CMD | {{TYPE_CHECK_CMD}} |
| TEST_CMD | {{TEST_CMD}} |
| BUILD_CMD | {{BUILD_CMD}} |
{{#IF ENABLE_TRANSLATION_CHECKS}}
### Translation Audit
Check {{LOCALES}} translation files in {{MESSAGES_DIR}}.
{{/IF ENABLE_TRANSLATION_CHECKS}}
Each project has an agent-vars.json that supplies the values:
{
"variables": {
"PRODUCT_NAME": "MyWritingTwin",
"PROJECT_ROOT": "/Users/emmanuel/Documents/Dev/MyWritingTwin",
"TYPE_CHECK_CMD": "npm run type-check",
"TEST_CMD": "npm run test:run",
"BUILD_CMD": "npm run build",
"LOCALES": "en, ja, fr, es",
"MESSAGES_DIR": "messages"
},
"toggles": {
"ENABLE_TRANSLATION_CHECKS": true,
"ENABLE_ANIMATION_ENV_CHECK": false
},
"blocks": {
"REPORT_TABLE": "| # | Check | Status |..."
}
}
The template engine supports three constructs:
- Simple variables โ
{{VAR_NAME}}replaced with the value fromvariables - Conditionals โ
{{#IF TOGGLE}}...{{/IF TOGGLE}}included or excluded based ontoggles - Blocks โ
{{BLOCK NAME}}replaced with multi-line content fromblocks
This means MyWritingTwin's quality-gate agent includes translation checks (because it's a multilingual app), while FluxDiagram's version skips them (single-language product). Same template, different output. When I improve the quality-gate logic, I update one template and it propagates to both projects on next sync.
The MyWritingTwin agent-vars.json has 127 variables, 16 toggles, and 11 blocks. It's the most complex project in the fleet. Others are simpler. But the system scales to arbitrary complexity because it's just JSON โ no special DSL, no compilation step, no runtime dependencies.
Deep-Merged Settings With Plugin Union Semantics
Settings files get special treatment. Instead of file-level replacement, Fleet deep-merges settings.base.json (common) with settings.project.json (project-specific). The merge has two special rules:
-
enabledPluginsuses union semantics. The base might enable five plugins. The project might enable two more. The merged result enables all seven. If the project explicitly sets a plugin tofalse, it's removed. This means projects can add plugins without repeating the entire base list. -
hooksuses full replacement. Project hooks completely override common hooks. This is intentional โ hook configurations are tightly coupled to a project's specific workflow, and partial merging would create unpredictable behavior.
# From fleet-sync.sh settings merge logic
for key, val in proj.items():
if key == "enabledPlugins":
# Union: base + project, project false removes
merged_plugins = dict(base.get("enabledPlugins", {}))
for plugin, enabled in val.items():
if enabled is False:
merged_plugins.pop(plugin, None)
else:
merged_plugins[plugin] = enabled
result["enabledPlugins"] = merged_plugins
elif key == "hooks":
# Full replacement
result["hooks"] = val
else:
result[key] = val
The generated settings.json is never edited directly. If you need to change settings, you edit settings.base.json (for all projects) or settings.project.json (for one project) and re-sync. settings.local.json โ which Claude Code also reads โ is never touched by Fleet, so you can still have machine-specific overrides that don't enter the fleet system at all.
The Nine Scripts
| Script | Lines | What It Does |
|---|---|---|
fleet-sync.sh | 694 | Push configs from fleet to projects. 6-state conflict detection, template rendering, settings merge. The core engine. |
fleet-test.sh | 948 | 161-test comprehensive suite. Tests registry, common layer, project layers, sync flow, conflict detection, pull, init, and promote โ all in an isolated temp directory. |
fleet-promote.sh | 395 | Take a project-specific resource and templatize it into a shared template. Extracts project-specific values into {{VAR_NAME}} placeholders. |
fleet-overview.sh | 319 | Dashboard showing project matrix, agent deployment map, resource counts, and promotion candidates. |
fleet-pull.sh | 290 | Reverse sync โ capture changes made directly in project .claude/ directories back to fleet. |
fleet-uplift.sh | 274 | Detect and remove redundant copies. If a project has files that are identical to the global or common layer, uplift removes the redundant copy. |
fleet-status.sh | 251 | Show per-file sync state (SYNCED, MODIFIED, STALE, CONFLICT) for one or more projects. |
fleet-init.sh | 204 | Register a new project. Adds it to the registry, creates its fleet directory structure, and deploys the common layer. |
fleet-auto-commit.sh | 53 | Auto-commit fleet changes to version control. Called by the session hook. |
The largest script โ fleet-test.sh at 948 lines โ is almost entirely test definitions. Every test runs in an isolated temp directory, so the test suite never pollutes real project data. Registry entries created during testing are cleaned up in a trap handler.
Running the test suite:
$ fleet-test.sh
[Registry Tests]
PASS registry.json exists
PASS registry.json is valid JSON
PASS registry has version field
PASS registry has default_base
PASS registry has projects
PASS Project 'MyWritingTwin' registered and active
PASS Project 'FluxDiagram' registered and active
PASS Project 'GAM-Forecast-Tool' registered and active
...
[Common Layer Tests]
PASS common/ directory exists
PASS settings.base.json exists and valid
PASS hooks/ directory exists
PASS notify.sh exists and executable
PASS sounds/ directory exists
...
[Project Layer Tests]
PASS MyWritingTwin has project directory
PASS MyWritingTwin has agent-vars.json
PASS agent-vars.json is valid JSON
PASS agent-vars has variables section
PASS agent-vars has toggles section
...
[Sync Flow Tests]
PASS Sync creates .claude directory
PASS Common files deployed to project
PASS Settings merge produces valid JSON
PASS Template variables substituted correctly
PASS Conditional blocks processed correctly
...
[Conflict Detection Tests]
PASS SYNCED state detected correctly
PASS STALE state detected correctly
PASS MODIFIED state preserves local changes
PASS CONFLICT state warns without overwriting
PASS --force overrides MODIFIED files
...
======================================
Test Results
======================================
PASS: 161
FAIL: 0
SKIP: 0
Total: 161
All tests passed!
The Fleet Dashboard
fleet-overview.sh produces a project matrix that shows the state of every project at a glance:
Fleet Overview
โโโโโโโโโโโโโโ
Projects (19 registered, 18 active)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
MyWritingTwin production [common+project] 7 agents
FluxDiagram development [common+project] 2 agents
GAM-Forecast-Tool internal [common+project] 0 agents
gam-assistant internal [common] 0 agents
website production [common] 0 agents
costabeach production [common] 0 agents
languesociens.jp production [common] 0 agents
Tokyo-Sur-Mesure production [common] 0 agents
calendar-studio development [common] 0 agents
spreadpix development [common] 0 agents
ALLOUIS production [common] 0 agents
...
Agent Deployment Map
โโโโโโโโโโโโโโโโโโโโ
quality-gate โ MyWritingTwin, FluxDiagram
seo-monitor โ MyWritingTwin, FluxDiagram
daily-briefing โ MyWritingTwin
content-pipeline โ MyWritingTwin
product-bootstrap โ MyWritingTwin
user-lifecycle โ MyWritingTwin
aeo-infra โ MyWritingTwin
Resource Summary
โโโโโโโโโโโโโโโโ
Common layer: 11 files (hooks, sounds, settings, notify)
Project layers: 123 files across 10 projects
Agent templates: 7 templates in catalog
Total tracked: 134 files
This is the view that tells me at a glance whether anything needs attention. Are there agents that should be deployed to more projects? Are there projects that are only getting the common layer when they should have project-specific configs?
SessionStart: Auto-Everything
The most impactful design decision was making Fleet invisible. Every time I open Claude Code in any project, a SessionStart hook runs fleet-auto-init.sh. It does four things silently:
-
Auto-registers new projects. If Claude Code opens in a directory that isn't in the registry, Fleet adds it and deploys the common layer. No manual
fleet-init.shneeded. -
Uplifts redundant copies. If a project has files that are identical to global or common versions, the uplift removes the redundant copy. This happens when skills or commands get moved from project-level to global โ the old copies stick around until uplift cleans them.
-
Pulls local changes. If I edited a hook directly in a project, the pull captures it back to the fleet directory so it's tracked centrally.
-
Auto-commits. Any fleet changes are committed to version control.
The result: zero manual fleet management. I don't think about Fleet. I open projects, work in them, and Fleet keeps everything consistent in the background. The only time I run a fleet command manually is when I create a new agent template or want to check the overview dashboard.
The Journey: How It Got Here
The evolution was iterative, driven by concrete pain points.
Phase 1: rsync. A 40-line script. rsync -av ~/.claude/fleet/common/ $PROJECT/.claude/. Worked until it overwrote a hook I'd spent an hour customizing. Deleted it the same day.
Phase 2: fleet-sync with manifest. The insight that made everything work: record what you deployed, so next time you can tell the difference between "user edited this" and "fleet updated this." This was the 3-way diff idea โ compare the manifest (what was deployed last time), the fleet source (what Fleet wants to deploy now), and the project file (what's actually there). Three values, six possible states.
Phase 3: fleet-status and fleet-pull. Once the manifest existed, status was trivial โ just compute the state for each file and display it. Pull was the natural complement โ if I fixed a hook directly in a project, I needed a way to capture that fix back into fleet without manually copying files.
Phase 4: Agent templates. The quality-gate agent was deployed to two projects. I was maintaining two copies. When I improved the agent logic, I had to update both. Templates with variable substitution solved this โ one source, project-specific output. The template engine (conditionals, blocks, variables) was built in a single session.
Phase 5: fleet-promote. The inverse of templates: take a project-specific agent that works well and extract it into a reusable template. The script identifies project-specific values (paths, product names, URLs), replaces them with {{VAR_NAME}} placeholders, and creates the template plus a starter agent-vars.json.
Phase 6: Auto-init. The SessionStart hook that made Fleet invisible. Before auto-init, I'd open a new project and realize three days later that it didn't have the common hooks. Now it's automatic.
Each phase took one to three sessions to build. The test suite grew alongside โ I wrote tests for sync before I wrote sync itself, because the conflict detection logic has enough edge cases that manual testing would have been reckless.
The Numbers
Today's state:
- 9 scripts, 3,428 lines of Bash
- 161 tests, all passing
- 19 projects registered (18 active, 1 archived)
- 134 files tracked across
fleet/commonandfleet/projects/ - 7 agent templates with variable substitution
- 3 projects with project-layer overrides (the rest use common-only)
- 0 manual config operations per day โ SessionStart handles everything
The projects range from a 4-language SaaS platform with 7 agents (MyWritingTwin) to simple client sites that only need the common hooks and sounds. The system handles both without special cases.
What Other Claude Code Users Can Take From This
You don't need 19 projects to benefit from this pattern. The pain starts at two.
The moment you have a second project and you want the same hooks, the same agent quality, the same settings baseline โ that's when copy-paste becomes a liability. You fix a bug in one project and forget the other. You improve an agent prompt and the improvement stays local.
The key ideas that transfer:
-
Content-based diffing over timestamps. SHA256 manifests survive moves, renames, editor save-without-change, and cross-machine transfers. Timestamps don't.
-
Conservative defaults. Never overwrite local changes unless explicitly told to. The system should assume your work matters more than consistency.
-
Global vs. deployable resources. Before building a sync system, check if some resources are already global. Claude Code's skill and command resolution saved Fleet from managing twice as many files.
-
Templates with variables, not forks. When two projects need the same logic with different values, parameterize โ don't copy. The maintenance cost of N copies grows linearly. The cost of one template stays constant.
-
Auto-everything via hooks. If a maintenance task should happen every time, make it happen without human intervention. The moment it requires remembering, it will be forgotten.
Fleet is built for my specific setup โ Bash scripts, macOS, Claude Code's .claude/ directory structure. But the pattern applies to any multi-project development environment where configuration consistency matters โ especially in SaaS companies managing multiple products or microservices. The hard part isn't the tooling. It's deciding that the cost of divergent configs is higher than the cost of building a system to prevent it.
For me, that decision was about thirty minutes into debugging a hook I'd already fixed in a different project.
I'm building MyWritingTwin.com โ an AI writing style system that captures your authentic voice for use with ChatGPT, Claude, and any LLM. If you're interested in how I run the entire operation as a solo founder with AI agents, check out the rest of the building-in-public series.