Last updated: v3.4.0
dotbabel is a verification layer for agentic development: skills that make an agent ground its claims in real source, and gates that check the result.
Two paths — pick yours:
| I want… | Path |
|---|---|
| Skills & commands in every Claude Code session | Dotfile bootstrap — 30 seconds, no npm required |
| The CLI in my own repo (verification gates, local attestation, doctor) | This page — 10 minutes, Node ≥ 20 required |
cd your-project
npm install --save-dev @dotbabel/dotbabel
The package has zero runtime dependencies. It registers seven bins under
node_modules/.bin/:
harness
dotbabel-doctor
dotbabel-detect-drift
dotbabel-init
dotbabel-validate-specs
dotbabel-validate-skills
dotbabel-check-spec-coverage
dotbabel-check-instruction-drift
npx dotbabel-init --project-name your-project --project-type node
This writes:
.claude/settings.json, .claude/settings.headless.json, .claude/skills-manifest.json.claude/hooks/guard-destructive-git.shdocs/repo-facts.json, docs/specs/README.md.github/workflows/{ai-review,detect-drift,quality,test,validate-skills}.ymlgithooks/pre-commit, githooks/pre-pushEvery placeholder (,, ``) is
substituted at scaffold time.
npx dotbabel-doctor
You should see ✓ rows for env, repo, facts, manifest, specs, drift, and hook.
The first run may warn about missing artifacts (e.g. docs/specs/ empty) —
that’s expected until you draft your first spec.
A final row reports check-on-stop trust. On a fresh repo it reads “no trust allowlist”, which is informational and never fails the run — turn-end project checks are simply off until you opt in. See hooks.md.
Use the /spec skill (if you’re in a Claude Code session) or scaffold
manually:
docs/specs/my-first-feature/
├── spec.json
└── spec.md
Minimum viable spec.json:
{
"id": "my-first-feature",
"title": "My first feature",
"status": "draft",
"owners": ["Your Name"],
"linked_paths": ["src/my-feature/**"],
"acceptance_commands": ["npm test"],
"depends_on_specs": [],
"active_prs": []
}
Validate it:
npx dotbabel-validate-specs
Green. You’re done.
In GitHub branch protection, require the shipped workflows:
validate-skills — manifest + drift + specstest — the pr quality profile, plus dotbabel-criteria for acceptance
criteria. The criteria job runs even when a local attestation lets the
verify job skip, because it is the independent second opinion.detect-drift — flags stale .claude/commands/*.mdai-review — PR review (optional)quality is not a PR gate: it runs the deep profile weekly and on manual
dispatch, since mutation and race detection are too slow for every push.
Both test.yml and quality.yml execute the project commands your repository
declares. Delete either one if you would rather adopt it later.
To run the same quality check before every push, opt in to the hook — it is not enabled by scaffolding, because it runs repository code:
git config core.hooksPath githooks
Only a policy failure blocks a push; a missing tool or a timeout prints a notice and gets out of the way.
Any PR touching a protected path (see docs/repo-facts.json) must now carry
a Spec ID: or ## No-spec rationale section. dotbabel-check-spec-coverage
enforces it.
If your repo has .claude/commands/*.md and .claude/skills/* that you want
visible to Codex, Gemini, Antigravity, OpenCode, and Copilot — not just Claude
Code — wire them up with project-sync. This is repo-local; user-scope artifacts stay in
~/.claude/ etc. via dotbabel bootstrap.
cd ~/projects/my-app
# 6a. One-time scaffold (writes .dotbabel.json + a starter CLAUDE.md if missing)
npx dotbabel project-init
# 6b. Preview, then apply
npx dotbabel project-sync --dry-run
npx dotbabel project-sync
# 6c. Verify (CI-safe, read-only)
npx dotbabel check-project-sync
What lands where:
| Source | Codex / Gemini / OpenCode destination | Copilot destination |
|---|---|---|
.claude/commands/<name>.md |
.codex/skills/<name>/SKILL.md (symlink) |
.github/prompts/<name>.prompt.md (generated, frontmatter mapped) |
.claude/skills/<id>/SKILL.md |
.codex/skills/<id>/ (whole-dir symlink) |
.github/instructions/<id>.instructions.md (generated, frontmatter mapped) |
CLAUDE.md (rule-floor block) |
rendered into AGENTS.md + GEMINI.md |
rendered into .github/copilot-instructions.md |
What Codex and Gemini get. Every Codex/Gemini destination is a symlink to
the Claude source file, not a translated copy — they read their own
frontmatter shape, so Claude-shaped frontmatter (allowed-tools, model,
effort, disable-model-invocation, the auto-routing description) is not
honored. Expect only direct slash invocation by name: /commit works, but
natural-language auto-routing, tool restrictions, and model selection apply
in Claude Code alone. A command that describes a Claude-only flow (headless
Claude workers, Claude-specific flags) fans out verbatim unless you list it
in cli_excluded below. Tracked at
#219.
What Copilot gets. Unlike Codex/Gemini, Copilot’s targets are generated
files with mapped frontmatter — description, name, argument-hint, and
tool grants carry over correctly. model, effort, and
disable-model-invocation still have no Copilot equivalent and are dropped
with a warning naming the file and the key. See
docs/copilot-frontmatter-mapping.md for
the full key-by-key table. A generated file that is hand-edited is backed up
before the next sync overwrites it.
.dotbabel.json is optional — without one, project-sync uses defaults
(fan_out: ["codex", "gemini", "antigravity", "opencode", "copilot"], the
standard target list, no cli_substitutions). When CLAUDE.md has no <!-- dotbabel:rule-floor:begin -->
markers, the whole file becomes the rule floor.
Add $schema to the top of the file for editor autocomplete and validation:
{
"$schema": "https://dotbabel.dev/schemas/dotbabel.config.schema.json",
"fan_out": ["codex", "gemini", "antigravity", "opencode", "copilot"],
"fan_out_layout": "per-cli",
"gate_on_cli_presence": true
}
fan_out accepts only codex, gemini, antigravity, opencode, and
copilot. A typo such as co-pilot fails with CONFIG_UNKNOWN_CLI instead of being skipped.
fan_out_layout (default per-cli) decides whether Codex, Gemini, and OpenCode
get one tree each or share a canonical one. Under shared, the table above collapses:
.claude/ fans out once to .cli/skills/, and .codex/skills,
.gemini/skills, and .opencode/skills become symlinks to it, so each command
and skill is tracked once instead of three times. Copilot’s .github/ shapes are unchanged. Switching an
existing repo backs the old trees up to .codex/skills.bak-<timestamp>; an
unknown value fails with CONFIG_UNKNOWN_LAYOUT. Revert to per-cli if a CLI
will not follow the redirect.
Antigravity keeps its own tree even under shared. It reads
.agents/skills/, a directory Codex and Gemini do not read, so a redirect there
would point at a tree it never follows. Only CLIs that can read the same
canonical tree share one; Antigravity is fanned out separately in both layouts.
Antigravity and Gemini CLI coexist. They are separate runtimes with separate
skills directories — .agents/skills/ and .gemini/skills/ per repo,
~/.gemini/config/skills/ and ~/.gemini/skills/ globally — and dotbabel never
migrates one into the other. What they do share is the instruction file: both
read GEMINI.md, so it is generated once rather than per CLI. Set
ANTIGRAVITY_CONFIG_HOME to relocate the global Antigravity root, exactly as
GEMINI_HOME relocates Gemini’s.
OpenCode gets a native tree even though it can read Claude’s. OpenCode
v2.0.5 discovers .claude/skills/ and .agents/skills/ through a
compatibility layer, so in principle it needs no fan-out of its own. dotbabel
writes .opencode/skills/ anyway: that compatibility layer is OpenCode’s
accommodation of other tools, not a contract dotbabel controls, and the native
tree keeps the wiring working if it ever narrows. Unlike Antigravity, OpenCode
can join the shared tree — it follows a symlinked skills root — so under
shared it takes a redirect like Codex and Gemini.
OpenCode’s user scope is XDG-based. Both global artifacts live under one
root: AGENTS.md sits beside skills/. The root is $OPENCODE_CONFIG_DIR, or
$XDG_CONFIG_HOME/opencode, or ~/.config/opencode, in that order — note that
OPENCODE_CONFIG_DIR names the root outright while XDG_CONFIG_HOME names its
parent. OPENCODE_CONFIG points at a config file and moves nothing. There is
no OPENCODE_HOME.
OpenCode reads the project AGENTS.md. It is the same file Codex and
Copilot read, generated once, not a third copy. Commands reach OpenCode as
<name>/SKILL.md inside the skills tree, the same shape Codex and Gemini get;
dotbabel does not write .opencode/command/, and note that OpenCode’s
Claude-compatibility covers skills only — it does not read .claude/commands/.
gate_on_cli_presence (default true) skips a CLI’s symlink fan-out when its
binary is absent from PATH. check-project-sync applies the same gate, so it
does not report the un-synced CLI as drift and instead prints
skipped <cli>: not on PATH. Pass --all to either command to inspect every
CLI in fan_out regardless. Instruction files are always written, never gated.
cli_excluded (default {}) maps a CLI to the command basenames and skill ids
it must not receive:
{
"cli_excluded": { "codex": ["review-prs-parallel"], "copilot": ["review-prs-parallel"] }
}
The sync skips those entries for that CLI and removes a link it wrote on an
earlier run; check-project-sync reports a lingering one as
stale (excluded but present). A name that matches nothing warns, an unknown
CLI key fails with CONFIG_UNKNOWN_CLI, and any other shape fails with
CONFIG_INVALID_EXCLUSION. Under fan_out_layout: "shared" Codex and Gemini
read one tree, so an exclusion for either applies to both and the sync warns
when their lists differ.
A repo with .dotbabel.json will also be picked up by dotbabel doctor —
the diagnostic adds a project-sync wiring check.
--json schema.ERROR_CODE.