You know that feeling when you update something important in one place, then you realise — oh no, I need to propagate this to like 10 other projects?
Yeah. That was me last week.
A Bit of Background
I maintain a Laravel kickstart project called Kickoff. Every Laravel project I start — personal, client, internal — starts from this base. It contains my opinionated project structure, conventions, Docker setup, and one particularly important file:
stubs/CLAUDE.md
If you're using Claude Code, this file matters a lot. It's the instruction manual Claude reads before it touches your codebase. Stack, conventions, patterns, boundaries — it's all there. Without it, Claude guesses. With it, Claude codes like it already knows your project.
Problem: I have multiple projects built on top of Kickoff. Every time I update stubs/CLAUDE.md in Kickoff, those changes need to flow downstream. Into every project. One by one.
I did it manually twice. Third time, I refused.
The Boring Fix vs The Right Fix
The boring fix: write a bash script that loops through directories and overwrites CLAUDE.md. Quick. Fragile. Dumb.
The problem with dumb scripts? They don't understand context. My projects aren't identical. Each one has project-specific sections in CLAUDE.md — custom domain notes, environment variables, Docker services, known issues, client-specific conventions. A dumb overwrite nukes all of that.
What I actually need is something that can:
- Scan a parent directory for projects
- Figure out which ones are based on Kickoff
- Diff their
CLAUDE.mdagainst the latest from Kickoff - Merge the changes without destroying project-specific content
That's not a bash script. That's judgment. And Claude Code is actually good at that — if you give it the right structure upfront.
So I built a Claude Code skill.
What's a Claude Code Skill?
A skill in Claude Code is a SKILL.md file that provides Claude with structured context, instructions, and a defined workflow for a specific repeatable task.
Think of it like this — instead of you typing a long prompt every single time, you encode the decision tree once into a skill file. Claude reads it, understands what it's supposed to do, and executes consistently every time.
I've been building these skills in my own repo: nasrulhazim/agent-skills. I already have skills for testing, Livewire components, API lifecycle, CI/CD pipelines, and more. This new one — project-sync — joins the collection.
A skill is just a directory:
skills/project-sync/
├── SKILL.md # The instructions
└── references/
├── merge-algorithm.md # How to merge sections
├── section-classification.md # What's shared vs project-specific
├── detection-markers.md # How to identify Kickoff projects
└── registry-schema.md # Registry file format
No runtime. No API. No build step. Just markdown that Claude reads and follows.
What the Skill Actually Does
Here's the workflow, broken into commands:
Scan: Find All Kickoff Projects
/project-sync scan ~/Projects --since=2025
Claude scans the directory tree, finds Laravel projects (checks for artisan + composer.json), then verifies Kickoff-specific markers:
-
app/Models/Base.phpexists -
composer.jsonrequirescleaniquecoders/traitify -
support/helpers.phpin autoload -
CLAUDE.mdmentions "Kickoff"
At least one marker must match. This avoids false positives from vanilla Laravel projects.
Output:
Scanned: ~/Projects
Found: 12 Kickoff Laravel projects (filtered: --since=2025)
~/Projects/2025/ — 8 projects
✓ project-alpha CLAUDE.md: 18.2 KB
✓ project-beta CLAUDE.md: 22.1 KB
✗ project-gamma No CLAUDE.md
~/Projects/2026/ — 4 projects
✓ project-delta CLAUDE.md: 19.8 KB
...
It can also scan a GitHub account without cloning anything:
/project-sync scan gh:cleaniquecoders --since=2025
Everything gets saved to a registry at ~/.claude/projects/.project-sync.json — persistent across Claude Code sessions.
Status: What Needs Updating?
/project-sync status
Source: kickoff/stubs/CLAUDE.md (fetched from GitHub)
Project Status Size Last Synced
─────────────────────────────────────────────────────────
project-alpha ✓ current 18.2 KB 2026-03-10
project-beta ✗ outdated 22.1 KB 2026-02-15
project-gamma ⚠ missing — never
project-delta ✗ outdated 19.8 KB 2026-03-01
Summary: 1 current, 2 outdated, 1 missing CLAUDE.md
Instantly see which projects have drifted.
Diff: Preview Before You Touch Anything
/project-sync diff project-beta
Dry-run merge. No files changed. Just shows what would happen:
## Sections to UPDATE (from source):
- Architecture & Key Concepts > Models - CRITICAL [changed]
- Architecture & Key Concepts > Enums [changed]
- Livewire Patterns > Toast Notifications [changed]
## Sections to MERGE (combine items):
- DO list: +2 new items from source, 3 project-only preserved
- DON'T list: +1 new item from source, 1 project-only preserved
## Sections PRESERVED (project-specific):
- Project Overview
- Common Commands
- Packages
- Docker Services
- Environment Variables
No surprises. You see exactly what gets replaced, what gets merged, and what stays untouched.
Update: Do the Thing
/project-sync update all
Sync complete:
✓ Updated: 8 projects
— Skipped: 2 projects (already current)
✗ Failed: 1 project (git working tree dirty)
Report saved: ~/.claude/projects/reports/20260314.0600/
Each updated project gets a clean commit: docs: sync CLAUDE.md with kickoff conventions.
The Secret Sauce: Section-Level Merging
This is the part that makes it actually work. Not line-level diffing — section-level merging.
A CLAUDE.md built from Kickoff has a predictable structure. Every H2/H3 section falls into one of four categories:
| Type | Action | Examples |
|---|---|---|
| SHARED | Replace from source | Model conventions, testing rules, Livewire patterns |
| PROJECT-SPECIFIC | Never touch | Project overview, packages, env vars, Docker config |
| MERGE | Combine both lists | DO/DON'T — keep source items + project-only items |
| CONDITIONAL | Check each subsection | Important Conventions (mixed shared + project-specific) |
How a DO List Merge Works
Source (from Kickoff):
- ✅ DO extend App\Models\Base for all models
- ✅ DO use dual-key pattern
- ✅ DO use Pest syntax for tests
- ✅ DO use Form Requests for validation ← NEW
Target (project-beta has custom items):
- ✅ DO extend App\Models\Base for all models
- ✅ DO use Pest syntax for tests
- ✅ DO use PostgreSQL for all new tables ← PROJECT-ONLY
- ✅ DO prefix admin routes with /admin ← PROJECT-ONLY
Merged result:
- ✅ DO extend App\Models\Base for all models
- ✅ DO use dual-key pattern ← RESTORED FROM SOURCE
- ✅ DO use Pest syntax for tests
- ✅ DO use Form Requests for validation ← NEW FROM SOURCE
- ✅ DO use PostgreSQL for all new tables ← PRESERVED
- ✅ DO prefix admin routes with /admin ← PRESERVED
Source items are updated and new ones added. Project-only items are preserved. Nobody loses anything.
The Section Classification Map
Here's the actual map encoded in the skill:
| Section | Classification |
|---|---|
## Project Overview |
PROJECT-SPECIFIC |
## Common Commands |
PROJECT-SPECIFIC |
## Architecture & Key Concepts |
SHARED |
## File Organization |
SHARED |
## Testing with Pest |
SHARED |
## Livewire Patterns |
SHARED |
## Important Conventions |
CONDITIONAL |
## Code Quality Checklist |
SHARED |
## Packages |
PROJECT-SPECIFIC |
## Docker Services |
PROJECT-SPECIFIC |
## Environment Variables |
PROJECT-SPECIFIC |
## Gotchas |
CONDITIONAL |
| Any unlisted H2 section | PROJECT-SPECIFIC |
That last row is important — if a project adds a custom section like ## Deployment Notes or ## Client Requirements, the skill automatically preserves it because it's not in the source.
Edge Cases
Because real-world projects are messy:
| Scenario | What Happens |
|---|---|
Project has no CLAUDE.md
|
Creates one from source + composer.json metadata |
| Dirty git working tree | Skips the project, reports as failed |
| Source URL unreachable | Falls back to cache, or asks for --source override |
| Custom H2 sections in project | Always preserved — if it's not in source, it's yours |
| Merged file exceeds 40 KB | Auto-refines (trims whitespace, condenses examples), then asks if still over |
| Sections in different order | Preserves project's order — merge is content-based, not position-based |
Why This Matters Beyond CLAUDE.md
The real point here isn't about CLAUDE.md specifically. It's about a pattern shift.
Every time I find myself doing something tedious and repetitive — especially something that involves judgment, not just mechanics — I now ask one question:
Can I encode this into a skill?
Same instinct that makes me write packages instead of copy-pasting code. Same instinct that makes me extract traits and contracts instead of duplicating logic. Except now, instead of PHP abstractions, I'm building AI workflow abstractions.
The mental model:
Tedious thing detected
→ Solve it once manually, understand the decision tree
→ Encode the decision tree into a skill
→ Never think about it again
Practical Steps If You Want to Do This
Start with the boring problem. What do you keep doing manually that involves some judgment?
Map out the decision tree before writing anything. What are the conditions? What are the outputs? What needs human review vs what's automatable?
Write the SKILL.md. Phases, inputs, workflow, edge cases. Be explicit. Claude follows structure well.
Keep it in version control. I keep mine in
nasrulhazim/agent-skills. Commit it, tag it, evolve it.Test it on a safe project first. Dry run. Verify the output before letting it touch 10 projects.
Try It Yourself
Install all my skills:
curl -fsSL https://raw.githubusercontent.com/nasrulhazim/agent-skills/main/install.sh | bash
Or just this one:
git clone https://github.com/nasrulhazim/agent-skills.git
cp -r agent-skills/skills/project-sync ~/.claude/skills/
Then in Claude Code:
/project-sync scan ~/Projects
/project-sync status
/project-sync diff project-name
/project-sync update all
Final Thought
AI tools aren't magic. But when you invest time designing how you use them — giving them structure, context, repeatable workflows — they become serious force multipliers.
The leverage isn't in using AI. The leverage is in designing how you use AI.
Working smart isn't about working faster. It's about doing the tedious thing once, then letting the system carry it forward.
That's the whole game.
My agent skills collection (26 skills, MIT licensed) → github.com/nasrulhazim/agent-skills
Kickoff (Laravel kickstart) → kickoff.my
Top comments (0)