A personal framework for packaging reusable Claude Code agents, skills and commands into a versioned catalog and installing them into any project with a single command.
Built around two ideas:
- Compose, don't copy. Define agents/skills/commands once. Group them into presets. Reuse across projects.
- Project-level overrides. A project can
disable,addorpatchany inherited artifact, so the framework adapts to projects that don't follow your default conventions.
After a few months using Claude Code, you accumulate agents, skills, commands and architectural conventions that work for you. Every new project pays the same tax: copy a dozen markdown files, edit a couple, forget which version is the canonical one, repeat.
This framework is the answer: a single source of truth in a git repo,
a CLI that materializes the right combination into .claude/ of any
target project, and an override mechanism so projects can deviate
without forking the catalog.
Three layers, in increasing project-specificity:
┌─────────────────────────────────────────────────────────────┐
│ Catalog presets/ agents/ skills/ git-hooks/
│ (this repo) *.yaml *.md *.md commit-msg…
└─────────────────────────────────────────────────────────────┘
│
│ resolveExtends + applyOverrides
▼
┌─────────────────────────────────────────────────────────────┐
│ Project manifest .claude-fw.yaml in the target project │
│ └ preset: <name> │
│ └ overrides: [disable, add, patch] │
└─────────────────────────────────────────────────────────────┘
│
│ claude-fw install
▼
┌─────────────────────────────────────────────────────────────┐
│ Materialized output .claude/ agents, skills, commands, │
│ (gitignored) settings.json, CLAUDE.md │
│ .githooks/ commit-msg, pre-commit, │
│ pre-push (chmod 0755) │
└─────────────────────────────────────────────────────────────┘
Git hooks are the one artifact type that lands outside .claude/:
they go to .githooks/ and install points core.hooksPath at that
folder (only when it is unset — an existing value is never
overwritten).
A preset declares a list of artifact ids and can extend other presets. Resolution flattens the chain (parent ids before child ids, deduplicated). Overrides apply on top of that resolved preset:
# .claude-fw.yaml in a target project
preset: react-native
overrides:
- disable: agent:plane-pm # this repo doesn't use Plane
- add: agent:hexagonal-test-reviewer # not in any preset by design
- patch: skill:commit-style # different conventions here
content: |
---
name: commit-style
---
Custom body…Every install writes a lockfile — .claude-fw.lock.json — recording
the content hash of each materialized artifact. The next install
compares the catalog against that lockfile and applies only the
difference: new artifacts are written, changed ones updated, and
artifacts dropped from the preset are deleted. Files you placed in
.claude/ by hand are never touched — the engine only manages what the
lockfile tracks. Re-running install is therefore safe and idempotent;
a file deleted by accident gets restored.
claude-fw status reports that same diff (added / updated / removed /
unchanged) without writing anything — useful before pulling a newer
version of the catalog into a project.
Inside this repo:
pnpm install
pnpm -r build
CFW_CATALOG_PATH=. node packages/cli/dist/index.js install --project .The last line is the framework configuring itself: the artifacts
declared in presets/base.yaml are loaded from agents/, skills/
and git-hooks/, then materialized under .claude/ and .githooks/.
CFW_CATALOG_PATH is the dev-flow override that makes the working
copy win over the catalog embedded in the binary.
To install into another project:
CFW=/path/to/claude-personal-framework
cd /path/to/your/project
CFW_CATALOG_PATH=$CFW node $CFW/packages/cli/dist/index.js init --preset base
CFW_CATALOG_PATH=$CFW node $CFW/packages/cli/dist/index.js installCommands: init writes the manifest, install materializes the
preset, list enumerates the catalog, status shows drift against
the last install, detect reports what a path is, and detect-stack
ranks presets by how well their detects: rules match a project. All
of them accept --json for programmatic consumers (the desktop app
consumes exactly that).
(A global bin install is on the roadmap; for now invoke via node.)
pnpm install
pnpm -r build
pnpm -C apps/desktop tauri:devOn Linux/Wayland prepend WEBKIT_DISABLE_DMABUF_RENDERER=1 if the
window fails to open with a GDK protocol error. See
apps/desktop/README.md for the full setup
(stack, IPC bridge, other gotchas).
First-time users land on a two-step welcome wizard. The first step picks
a project folder and detects the stack against the catalog's detects:
rules to preselect the right preset:
The second step shows the setup summary and runs init + install in
sequence; if the folder is missing or not a git repo, the wizard
intercepts the typed error and offers the right native modal:
Once a project is set up, the app switches to the free mode: project header with a Switch dropdown, plus three cards for Status / Catalog / Actions. Outcomes are ephemeral — success banners auto-dismiss after 5 s, errors stay sticky:
When the welcome flag is set and there's no active project, the recent projects screen replaces the wizard:
Settings is a full-screen modal that manages the catalog sources — built-in toggle, user folders with per-row remove, and an Add folder button that validates against the engine before persisting. The welcome wizard can be reset from the bottom section:
Hexagonal (ports & adapters):
packages/core/src/
├── domain/ ← entities, value objects, domain services
│ ├── model/ Agent, Skill, Command, GitHook, Preset,
│ │ Composition, Settings, Instructions,
│ │ ContentHash, Override, DetectRule, ids,
│ │ ProjectManifest, Lockfile, DriftReport
│ ├── errors/ DomainError + typed subclasses
│ └── services/ resolveExtends, applyOverrides, computeDrift,
│ evaluateDetects, computeGitignoreBlock, dedupe
├── application/ ← use cases + ports + shared services
│ ├── ports/ Catalog, Writer, LockfileStore, ManifestStore,
│ │ ProjectInspector, StackInspector, GitConfig,
│ │ Gitignore, PathProbe
│ ├── services/ buildComposition (shared by install + status),
│ │ AggregatedCatalog (multi-source precedence)
│ └── use-cases/
│ ├── init-project/ writes .claude-fw.yaml
│ ├── install/ drift-aware materialization
│ ├── check-status/ same diff, read-only
│ ├── list-catalog/ enumerate the catalog
│ ├── detect-path/ is this a framework root? a project?
│ └── detect-stack/ rank presets against a project
└── infrastructure/ ← adapters that implement ports
├── yaml/ parse/serialize preset + project manifest
├── json/ parseLockfile, serializeLockfile
├── markdown/ extractFrontmatterDescription
├── git/ ChildProcessGitConfig (core.hooksPath)
└── fs/ FsCatalogReader, ClaudeWriter, LockfileStore,
ManifestStore, FsGitignore, FsStackInspector,
LocalProjectInspector, PathProbe
packages/cli/ ← CLI port over the same engine
└── src/ 6 commands, each with a --json formatter
apps/desktop/ ← Tauri desktop port over the same engine
├── src/ React 19 + Vite + Tailwind 4
└── src-tauri/ Rust handlers that spawn the CLI as subprocess
The domain has zero filesystem or framework imports. Adapters depend inward on the domain. CLI and the desktop app are two independent ports over the same use cases — the desktop app does not reimplement the engine, it spawns the CLI in a subprocess and consumes its structured JSON output.
.
├── agents/ Catalog: 5 agents
├── skills/ Catalog: 18 skills (cross-cutting + per stack)
├── git-hooks/ Catalog: commit-msg, pre-commit, pre-push
├── presets/ Catalog: 6 preset YAMLs
│ ├── base.yaml everything below extends it
│ ├── nestjs.yaml react-native.yaml vue.yaml
│ └── laravel.yaml tauri-rust-react.yaml
├── packages/
│ ├── core/ Engine: domain + application + infrastructure
│ └── cli/ CLI port
├── apps/
│ └── desktop/ Tauri desktop port (React + Rust)
├── docs/
│ ├── adr/ Architecture Decision Records
│ └── roadmap.md Prioritized backlog (Now / Next / Deferred)
├── .claude/ Output of `claude-fw install` (gitignored)
├── .githooks/ Output of `claude-fw install` (gitignored)
├── .claude-fw.yaml Project manifest (this repo configures itself)
└── .claude-fw.lock.json Lockfile: content hashes of the last install
There is no commands/ directory yet — the engine supports the
artifact type end to end (CommandId, writeCommand, lockfile
tracking), no preset declares one so far. Same for instructions/,
which materializes as .claude/CLAUDE.md when a preset asks for it.
- ✅ Domain model + composition resolver (extends chains, diamond inheritance, cycle detection)
- ✅ YAML + JSON + filesystem adapters
- ✅ Lockfile-based drift: idempotent installs,
statusreports added / updated / removed / unchanged without writing - ✅ CLI with 6 commands, all with
--json - ✅ Artifact types: agents, skills, commands, git hooks, per-preset
settings.jsonandCLAUDE.md - ✅ Git hooks materialized to
.githooks/withcore.hooksPathactivation and take-over guards for user-authored hooks - ✅ Managed
.gitignoreblock in the target project so install output never gets committed by a straygit add .(ADR 0007) - ✅ Multi-source catalog with precedence
env > folder > builtin; the built-in catalog ships embedded in the desktop binary (ADR 0003, ADR 0004) - ✅ Stack detection (
detects:rules) driving preset preselection - ✅ Six presets: base + nestjs, react-native, vue, laravel, tauri-rust-react
- ✅ Self-hosting: this repo's own
.claude/and.githooks/are materialized by the engine - ✅ Tauri desktop app: welcome wizard, free mode, recent projects, settings panel (see apps/desktop/README.md)
- 🚧 Global bin install — today the CLI runs via
node …/dist/index.js - 🚧
overrides:field in Preset schema (ADR 0001) - 🚧 Provider-agnostic
pr-creator(ADR 0001) - 🚧 Sidecar bundling Node + CLI inside the desktop binary
The prioritized backlog, with the trigger condition for each deferred
item, lives in docs/roadmap.md.
Recorded in docs/adr/. Each record captures the
context, the options considered (including the rejected ones), the
decision and its consequences.
pnpm install # install deps
pnpm -r test # run tests
pnpm -r build # type-check + emit dist/
pnpm lint # biome check
pnpm check # biome check --write (fix formatting)For the desktop app specifically, see apps/desktop/README.md — there is a Linux/Wayland environment variable that may be required.
TypeScript strict (noUncheckedIndexedAccess,
exactOptionalPropertyTypes, verbatimModuleSyntax). Biome for lint
- format. Vitest for tests on the engine.




