MyWritingTwin
Get Writing Twin
โ†Back to Building

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.

Agentic BusinessClaude CodeDeveloper Tools
Share:

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:

StateMeaningAction
SYNCEDFile unchanged since last sync, fleet unchangedSkip (no-op)
STALEFleet updated, project copy unchangedDeploy (safe update)
MODIFIEDUser customized locally, fleet unchangedSkip (preserve work)
CONFLICTBoth user and fleet changedSkip and warn
NEWFile doesn't exist in project yetDeploy
MISSINGWas deployed, now deleted from projectLog 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:

  1. Simple variables โ€” {{VAR_NAME}} replaced with the value from variables
  2. Conditionals โ€” {{#IF TOGGLE}}...{{/IF TOGGLE}} included or excluded based on toggles
  3. Blocks โ€” {{BLOCK NAME}} replaced with multi-line content from blocks

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:

  1. enabledPlugins uses 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 to false, it's removed. This means projects can add plugins without repeating the entire base list.

  2. hooks uses 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

ScriptLinesWhat It Does
fleet-sync.sh694Push configs from fleet to projects. 6-state conflict detection, template rendering, settings merge. The core engine.
fleet-test.sh948161-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.sh395Take a project-specific resource and templatize it into a shared template. Extracts project-specific values into {{VAR_NAME}} placeholders.
fleet-overview.sh319Dashboard showing project matrix, agent deployment map, resource counts, and promotion candidates.
fleet-pull.sh290Reverse sync โ€” capture changes made directly in project .claude/ directories back to fleet.
fleet-uplift.sh274Detect 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.sh251Show per-file sync state (SYNCED, MODIFIED, STALE, CONFLICT) for one or more projects.
fleet-init.sh204Register a new project. Adds it to the registry, creates its fleet directory structure, and deploys the common layer.
fleet-auto-commit.sh53Auto-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:

  1. 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.sh needed.

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

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

  4. 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/common and fleet/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:

  1. Content-based diffing over timestamps. SHA256 manifests survive moves, renames, editor save-without-change, and cross-machine transfers. Timestamps don't.

  2. Conservative defaults. Never overwrite local changes unless explicitly told to. The system should assume your work matters more than consistency.

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

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

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

Share:

Comments

Loading comments...

Leave a comment

Your email will not be displayed publicly.