Documentation

Start here, install, set up a Forge: then each capability with the problem it addresses, every command it adds and how it works. The home page lists each. working marks what runs today and is covered by tests; a # planned comment in a snippet marks a command that is designed, not built; mvp marks the minimum first release (on a target, as for OpenCode in the matrix, it means loaded by the tool but not yet used by a model session).

Start here

Start here

what it iscraftar keeps one AI-coding harness (rules, agents, commands, skills, hooks and MCP servers) in a Git repository, the Forge, and writes each tool's own files from it into every workspace, recording them in craftar.lock.

craftar 1.6.0 on a synthetic workspace: import an existing .claude/, see that nothing would change, add targets and sync. The file list is shortened (the run wrote 22 files), the run's scratch home is shown as ~, and the # line stands for the edit that adds the targets to craftar.yaml.

Import first: craftar import turns an existing workspace into a Forge and a craftar.yaml, and craftar diff proves nothing would change. Then add targets and run craftar sync. No Forge yet? See Setup.

How byte fidelity is checked: the oracle imports a hand-built workspace (Claude Code + Kiro), syncs it and asserts every generated file byte for byte, then that a second sync is a no-op, a hand edit survives as drift and a removed ingredient disappears.

Install

Install

needsNode 22 or later, and git for a Forge in a repository.

$ npx craftar --version # or run any command through npx $ craftar doctor # what is off on the machine and in the workspace, with the fix

Stack: TypeScript / Node CLI (commander, zod, yaml) with its own emitter per target; rulesync stays a reference for the formats, not a dependency.

craftar for Orca, the Orca plugin: Get the Orca plugin →. Its README says how to install it and what approving it grants.

Setup since 1.7.0

Setup: a new Forge and its first workspaces

no Forge yetcraftar setup creates a Forge (a new Git repository with one commit) and starts the first workspaces on it. It needs craftar 1.7.0 or later; with a Forge already, start a workspace with craftar init --forge <dir> --profile <name>.

Measured on the 1.7.0 branch (llima/craftar-cli#130); the commit hash differs on every run.

$ craftar setup # in a terminal: asks where the Forge goes, its first profile, the workspaces and the targets $ craftar setup --forge-dir api-forge --profile acme-api --workspace acme-api --import # bring a workspace's .claude/ into the new Forge first

Setup only creates: a Forge directory that exists and is not empty is refused, with the craftar init command to use it instead. The Forge gets a README, a starter rule, the profile and a base recipe, and one commit with no remote; each workspace then gets its craftar.yaml and its first sync. The interactive form lists every target, each as working or mvp, and ends with the craftar setup line that repeats it.

Forges

Forges

the sourceA Forge is one Git repository holding every ingredient, recipe and profile. Workspaces point at it by path, or by git URL at a branch, tag or full SHA; it is never a remote of a team repository.

acme-forge/
├── craftar.forge.yaml        schema: 1 … 18, each level opt-in
├── ingredients/
│   ├── rules/<name>/         ingredient.yaml + rule.md
│   └── agents/ · commands/ · skills/ · hooks/ · mcp/ · scripts/ · steering/ · services/
├── recipes/                  base.yaml, stack recipes with a slot
└── profiles/<name>/          profile.yaml: recipes, targets, params, sections
$ craftar recipes --forge <dir> # the Forge's recipes and the profiles that use them $ craftar ingredients --forge <dir> # every ingredient, its type and targets $ craftar forge variants --forge <dir> # where clients differ, nearest first

An ingredient is a folder with an ingredient.yaml; a recipe bundles ingredients with parameters; a profile (one per client) picks recipes, targets and values. A second client imported into the same Forge reuses what matches and makes the rest variants, which forge unify folds back (see Growing the Forge and F2). A Forge declares the schema: it needs, and an older craftar refuses a level it does not know.

F1 workingmvp

Forge → workspace sync

problem · P1 · P2A change to any of A agents or their rules must reach T workspaces in K formats; today that is T × K manual edits with no provenance.

$ craftar sync [--dry-run] [--check] [--overwrite-drift] $ craftar diff [--exit-code] # what sync would change, orphans included; writes nothing $ craftar status # per file: new · update · unchanged · adopt · drift · collision · orphan · orphan-drift $ craftar sync --offline # remote Forge: fetched first; --offline reads the cached copy $ craftar cache prune [--dry-run] # cached Forges nothing uses, unfinished fetches, trees unused for 14 days $ craftar sync --targets kiro · craftar update # planned

One command resolves and emits for every active target and prints what it wrote, removed and skipped, file by file. With a git-URL Forge, ref follows a branch or pins a tag or full SHA; sync fetches first and fails when it cannot, so CI never syncs from a stale copy unless it passes --offline. craftar cache prune removes the Forge cache entries nothing uses (no registered workspace and not the current one) entries whose first fetch never completed, and trees unused for 14 days; it never touches a workspace, a lock or the registry, and --dry-run shows what it would remove. A symbolic link never carries sync out of the tree it works on: a Forge file read through a link that leaves the Forge or leads into its .git, and a workspace path that leaves through one, refuse the run before anything is written, there is no flag; a link that stays inside works as before. In a git repository, sync and sync --check warn for each generated file git ignores, naming the ignore rule by file, line and pattern; craftar never edits an ignore file, and mayBeIgnored in craftar.yaml lists the paths kept out of git on purpose.

← All capabilities
F2 workingmvp

Multi-target emission

problem · P1Each team's AI coder has its own file layout, frontmatter and agent schema. Translating by hand yields FM-4: mirrors that diverge.

$ craftar import --from claude-code # existing .claude/ → ingredients + recipes + profile $ craftar forge variants # where clients differ, nearest first $ craftar forge diff rule/deploy # hunks, each with a suggested class $ craftar forge unify rule/deploy --profile acme --plan plan.yaml # base, variant, param or section $ craftar forge impact # every registered workspace reading this Forge, and what its next sync would do $ craftar targets # the capability matrix: what each target does with each ingredient type

Three layers: the open standards, AGENTS.md (the always-on rules, then each scoped rule listed at the file a target writes for it or embedded when none does), Agent Skills (.agents/skills/<name>/SKILL.md, written by the agent-skills target) and mcp.json, written by the target emitters; tool-specific formats; Craftar post-processors for known gaps, e.g. injecting resources: file://.kiro/steering/*.md into Kiro custom agents, and writing each agent's permissions.rules, derived from its tools as kiro-cli 2.37 derives them, so Kiro no longer rewrites the file. Craftar's own emitters cover Claude Code, Kiro and AGENTS.md: working; the Kiro emitter reproduces a hand-built workspace byte for byte, including agent JSON and command mirrors, and, since 1.1.0 and 1.2.0, Cursor, Copilot, Agent Skills (.agents/skills/), Codex and Antigravity, working since 1.5.0, validated on 2026-11-07 by opening each tool from its CLI on a synthetic workspace: and, since 1.6.0, OpenCode, mvp: agents to .opencode/agents/ with permissions derived from their tools, commands to .opencode/commands/ and MCP servers to .opencode/opencode.json, its rules through AGENTS.md; OpenCode 2.2.5 loaded them through its local service, with no model, and it stays mvp until a model session uses those files. Copilot writes its MCP servers to .vscode/mcp.json for VS Code and to .github/mcp.json for the Copilot CLI; .github/prompts/ is VS Code's, and the Copilot CLI reads commands from .agents/skills/. Codex reads .codex/agents/ only in a trusted project, and .codex/hooks.json only once its hooks were reviewed, and Antigravity run headless never applied a glob rule. What a tool already reads in another target's file is not written twice: with agents-md among the targets, an always-on rule with the default targets is left to AGENTS.md by Cursor, Copilot and Antigravity, and with agent-skills, a command with the default targets is left to .agents/skills/ by Copilot; craftar explain on such a file names the file that holds it. A Forge file that names cursor, copilot or agent-skills in targets needs schema: 15, one that names codex or antigravity needs schema: 16, and one that names opencode needs schema: 18; a workspace may name any of them in craftar.yaml at any Forge schema. Since 1.3.0 a hook ingredient with a registration is written for Codex to .codex/hooks.json and for Antigravity to .agents/hooks.json; Cursor, the Copilot CLI and VS Code run the hooks .claude/settings.json holds, so no file of theirs is written. rulesync stays a reference for the formats, not a dependency. Importing a second client into an existing Forge reuses every ingredient whose render for that profile equals the workspace file, infers declared {{key}} values into the profile, and makes the rest variants; forge unify resolves each one back into its base (see Growing the Forge). craftar forge promote <type/name> --profile <p> goes the other way: it makes a profile's variant the base and gives the old base back, as a variant of its own, to every other profile that resolved it, gated on git, proved before the first write, never a commit. craftar import --report <file.md> also writes a Markdown report of the import: what was created, reused, made a variant or rejected, and the .kiro/ files that differ from what sync would generate, as paths and line counts, never content. craftar import --prefer-kiro <rule,…> takes the named rules from .kiro/steering/<name>.md instead of .claude/rules/<name>.md, for a steering mirror that was edited by hand and is the text to keep; the text is accepted only when it is proved to render back to that steering file's body for Kiro. Rules only: commands, agents and skills are not taken from Kiro. Import reads a Claude rule's paths: frontmatter as its scope, so a scoped rule stays scoped (fileMatch for Kiro, and a scoped rule in AGENTS.md) and keeps that frontmatter verbatim, so .claude/rules/<name>.md comes back byte for byte. At schema: 7 import never writes a generated rules index over its placeholder silently: a variant that holds the placeholder and whose file differs from what sync writes is refused, a new variant that freezes the list is warned, and --prefer-kiro refuses a rule whose ingredient holds the placeholder. Import takes nothing from behind a symbolic link that leaves the workspace or leads into its .git: such a source is rejected and listed, never adopted. In a Forge at schema: 8, a source equal to the importing profile's own local is reused there, one that differs is refused, and the Forge it would write is first loaded on a scratch copy. With --local=auto, a new source of the profile becomes a local there instead of a root ingredient, and a local that differs is rewritten; --local=always writes no ingredient at the root. A new local no recipe of the profile's folder cites is wired into the recipe <p>-local. With agents-md.primary: true, in a Forge that declares schema: 5 and a workspace whose targets include agents-md, an always-on rule with the default targets is written to AGENTS.md alone, with all three targets, instead of three times (.claude/rules/, .kiro/steering/ and AGENTS.md): its other files become orphans sync removes; the .claude/, .kiro/ and AGENTS.md texts that cite it point at its place in AGENTS.md; scripts, hooks and files copied as bytes are never rewritten: a reference left there is warned with its file and line; and generated Kiro resources name only the steering files the plan writes, while a hand-written entry naming a file the plan does not write and the disk lacks fails sync --check (a writing sync still writes). Craftar writes no CLAUDE.md; when one keeps Claude Code from reading AGENTS.md, every plan says so. The mode is opt-in (a workspace that does not set it sees no change) and every craftar before 0.36.0 refuses a schema: 5 Forge. A hook ingredient can declare its registration (the event, matcher and command Claude Code runs it with) and claude-code writes it to .claude/settings.craftar.example.json; craftar settings apply merges it into .claude/settings.json, only when a person types it, keeping every env value, permissions and every other key: or the block is copied by hand. No other command writes a settings file Claude Code reads. Every craftar before 0.38.0 refuses a Forge whose hook holds registration.

← All capabilities
F3 working

Layers and stack slots

problem · P1Teams differ in stack. Without composition, a React team inherits Angular reviewers, or every team gets a hand-pruned fork of the harness.

$ craftar add recipe frontend-react --replace · craftar remove recipe aws-local # edits craftar.yaml, never syncs $ craftar recipes · craftar ingredients --recipe frontend-angular # the catalogue: every recipe and ingredient, and what this workspace uses $ craftar explain .kiro/steering/workflow.md # which layer decided this file $ craftar forge localize rule/acme-deploy --profile acme · craftar forge share rule/acme-deploy --profile acme # schema 8: into a profile's folder and back

Recipes declaring the same slot are mutually exclusive. craftar add recipe and craftar remove recipe change the workspace's choice in craftar.yaml in place, refusing a recipe that would share a slot (unless --replace), removing one another recipe still brings in through extends, or an edit that would not resolve, and print what the next sync would do; they never sync. craftar recipes and craftar ingredients list the whole Forge read-only, mark what the workspace uses (and, for recipes, why), and --recipe shows what choosing one would bring. The profile sets the team default; the workspace may override; the local file (craftar.local.yaml, git-ignored) overrides the workspace for one developer; its arrays replace the workspace's. An ingredient body can also mark a client-specific block as a section (<!-- craftar:section flavors --> … <!-- /craftar:section -->): the block is the default, a profile's sections or the workspace's overrides.sections replace or empty it, no rendered body holds the markers, explain names the layer each section came from, and forge unify can extract a section from a variant (take: section). A Forge that uses sections declares schema: 2 or above, so an older CLI refuses it rather than leak the markers. In a Forge that declares schema: 6, an opening marker can name the targets that receive the block (<!-- craftar:section flavors targets=claude-code,kiro -->): a target it names gets the section as any other, a target it does not name gets nothing of it, whatever a profile or the workspace sets, and a marker with no targets= is read as before. A Forge that writes no targets= sees no change, and every craftar before 0.39.0 refuses a schema: 6 Forge. In a Forge that declares schema: 7, a line of a rule or steering body that holds {{craftar.rules.index}} alone becomes the list of the rules the profile resolves, as each target names them: up to three groups (Always on:, Scoped:, On request:) one line per rule with its description, or by its reference alone, warned, when it has none; a rule no target of the workspace writes is left out. explain names an indexed file's list, AGENTS.md aside. A placeholder not alone on its line, or in an agent, a command or a skill, is left as written and warned; so is one alone on its line below schema: 7. Either warning makes doctor --strict exit 1. The index is generated, never a parameter, and every key starting with craftar. is reserved: forge unify never extracts into one and import never infers one. A Forge that writes no placeholder and sets no craftar. key sees no change from the index, and every craftar before 0.40.0 refuses a schema: 7 Forge. In a Forge that declares schema: 8, a profile's folder holds its own ingredients and recipes (profiles/<p>/ingredients/, profiles/<p>/recipes/), which only that profile sees: they render exactly as they would at the root, a root recipe can never reach them, and a broken reference fails the load, naming the files. explain names where a file's ingredient lives. Every craftar before 0.45.0 refuses a schema: 8 Forge. craftar forge localize moves a root ingredient only one profile resolves into that profile's folder, with the root recipes that cite it, and craftar forge share moves a local back to the root: each gated on git and proved, before the first write, to change no byte any profile or registered workspace plans; neither commits. In a Forge that declares schema: 9, an ingredient's metadata takes {{param}} too (a rule's paths: and pattern, an agent's or a command's description) a value that would break the file it lands in fails the plan, and forge diff and forge unify work on those fields; every craftar before 0.47.0 refuses a schema: 9 Forge.

← All capabilities
F4 working

Workspace bootstrap

problem · P2Starting with a new team means reassembling repos, harness, templates and local services from memory.

$ craftar init --forge ../forge --profile team-a --targets claude-code,kiro # writes craftar.yaml, then runs the first sync $ craftar init # in a terminal: asks for the Forge, the profile, then the recipes and targets; confirms once $ craftar init --forge ../forge --new-profile team-e --like team-a # writes profiles/team-e/profile.yaml (not committed), then the workspace $ craftar repos · craftar repos clone # the profile's repositories, present or missing, then clone the missing ones

Writes craftar.yaml (the Forge, the profile, and the recipe and target choices, given as flags or as answers) only once the workspace resolves and plans, then runs the first sync, which registers the workspace. Files already on disk are adopted when they equal the plan and otherwise left as they are and reported; an existing craftar.yaml is never edited. In a terminal, craftar init asks for a --forge or a --profile it was not given, the Forge last synced on this machine offered, the profile from the Forge's list, then the recipes and targets to keep or adjust, confirms once before anything is written, and ends by printing the same run as flags, for CI; off a terminal a missing flag is refused. When a declared parameter a file the plan writes cites has no value, craftar.yaml is still written and the first sync is not run. With --clone it then clones each repository the profile's repos lists into the workspace: git clone over https or ssh only, with the git found on PATH, never over a file sync writes, into the Forge or out of the workspace; when init asks for what the flags do not give, it lists the URLs first and asks, the default no. A clone is recorded nowhere, and sync never updates or removes it. craftar repos lists those repositories in an existing workspace (present, missing or blocked, with their URLs) and craftar repos clone clones the missing ones. With --new-profile <name> (optionally --like <profile>), init first writes one new file in a Forge directory, profiles/<name>/profile.yaml (the recipes and targets chosen, nothing else of the template) behind a git gate and never committed, then starts the workspace on it; in a terminal the profile list offers 0) a new profile.

← All capabilities
F5 working

PM tool and internal platform per profile

problem · P1Agents need to read user stories and talk to the team's internal developer platform; each team uses a different tool for both.

In a Forge that declares schema: 3 or above, a profile's integrations block selects recipes and gives params. integrations.<slot>.kind: <kind> selects the recipe named <slot>-<kind>, integrations.pm.kind: azure-devops selects pm-azure-devops, a recipe that lists, say, the MCP ingredient and a rule describing where stories live, and every scalar of the block is the param integrations.<slot>.<key>, cited in a body as {{integrations.pm.org}}. An integration with no kind gives params only. A schema 3 Forge is read only by craftar 0.27.0 and later; below schema: 3 the block is ignored, without a warning; a kind with no recipe stops sync. In a Forge that declares schema: 4, an MCP server takes the params its ingredient declares, so one server serves every client: url: "{{integrations.idp.url}}", with integrations.idp.url declared in the ingredient's params, renders each profile's own URL, and .mcp.json and .kiro/settings/mcp.json get the same rendered server. A declared key with no value stops sync; a key the ingredient does not declare is left as written, with a warning when some layer gives it a value; below schema: 4 a server is written exactly as the Forge holds it. A schema 4 Forge is read only by craftar 0.28.0 and later. Tokens are referenced by authEnv, never stored (FM-8).

← All capabilities
F6 working

Local services per workspace

problem · P1Cloud dependencies (queues, buckets) need a local stand-in, and the agent has to know its endpoints and fake credentials.

$ craftar services up | down | status # Docker Compose on what the lock records

A service ingredient is a Docker Compose fragment plus the files it mounts. sync writes it under .craftar/services/<name>/, with the stack .craftar/compose.yaml (a Compose include of every fragment) for no target, text always LF without a BOM (a file listed as binary is copied as bytes), and records each file in craftar.lock (schema 3 only for a workspace that has a service). They are generated files like any other: status, diff and sync --check treat them the same. craftar services up reads the lock, not the Forge, and refuses to start a stack with a missing or hand-edited file, or with a stack or fragment the lock does not record; down stops it and status shows it. sync never runs Docker; up needs Docker Compose 2.20.0 or later. A Forge that holds a service ingredient is read only by craftar 0.25.0 and later.

← All capabilities
F7 list | install working

Project templates

problem · P1New projects should use the team's own dotnet new (or npm create, nx, cookiecutter) templates; agents need to know which.

$ craftar templates list | install # the dotnet new templates the profile declares$ /create-project # planned

A profile's templates block lists packages per kind (dotnet-new today) with an optional source feed. craftar templates list shows what the profile declares; craftar templates install runs dotnet new install once per entry, into the user's own template store. Nothing installs implicitly: not sync, not init, not doctor. An entry of a kind this CLI cannot install (npm-create, nx) is listed, skipped and warned. A source carrying a user or a password is refused; authentication is NuGet's. Planned: a generated rule mapping archetype → template, and a wrapping /create-project command.

← All capabilities
F8 working

Drift and reverse contribution

problem · P2Generated files get edited in place. Either the edit is lost on the next sync, or the generator is abandoned (FM-2).

$ craftar status # drifted files listed; never overwritten without --overwrite-drift $ craftar drift show # the hand-edited files, whether the Forge also changed each, whether the edit can be promoted; with a path, its diff $ craftar drift discard <path...> # a sync that also regenerates the named hand-edited files; their edits are lost $ craftar drift promote <path> # carry one hand edit of a .claude/ file into the Forge

Normalized hashes from the lock are compared with disk. drift discard is a sync that also regenerates the named hand-edited files and removes a named hand-edited file the Forge no longer produces. drift promote carries one hand edit of a .claude/ file into the Forge's working tree, a value in the profile's params or sections when the edit is only that, otherwise a variant for the workspace's profile, gated on git and proved before the first write; it never syncs and never commits. A file that holds an expanded rules index is not promotable: edit the Forge's rule, or the description of the rules it lists. So is a hand edit of a metadata field the Forge renders from a parameter, at schema: 9; drift show says why. At schema: 8, a hand edit of a profile-local file is promoted into the local itself: a value inference proves goes to the profile's params or sections, otherwise the local's own files are edited in place, with no variant. Sync direction is Forge → workspace; reverse flow is always explicit.

← All capabilities
F9 working

CI

problem · P2Sync that depends on someone remembering will lag by workspace.

$ craftar sync --check # exit ≠ 0 when out of sync or drifted$ craftar diff --exit-code # the same verdict as --check, with the diff that explains it$ craftar sync --check --committed [rev] · --check --index # the same check over a commit or git's index$ craftar git-hooks install [--pre-push] [--pre-commit] # an optional hook that runs it, only when typeduses: llima/craftar-cli/ci/github@<SHA or v-tag> # GitHub Action · Azure: template ci/azure/check.yml@craftar

AGENTS.md has a byte budget, agents-md.maxBytes (default 32768, Codex's project_doc_max_bytes; 0 turns it off): over it every plan warns, naming the largest rules, and when a recipe, the profile or the workspace sets it, sync --check, status and diff --exit-code with no path fail: a writing sync still writes. Two thin wrappers in the CLI's repository make a pull request fail when a workspace is out of step with its Forge: a GitHub Action (ci/github/action.yml) and an Azure Pipelines step template (ci/azure/check.yml). Each installs an exact craftar version, outside the checkout, runs sync --check (its exit code is the step's) and names the files out of sync as annotations and a job summary, never a file's content; they write nothing to the workspace. A private Forge's token comes only from the workflow, for an origin the workflow names. Pin them to a commit SHA or a v<version> tag: a change under ci/ reaches a branch reference the moment it merges. The action ran on GitHub's Linux and Windows runners; the Azure template has not run on a real agent. craftar sync --check --committed [rev] and --check --index give the same verdict over what git holds (a commit or the index) instead of the folder, writing nothing and starting no hook, filter or core.fsmonitor of the repository. craftar git-hooks install writes, only when typed, a pre-push (or pre-commit) hook that runs that check, only inside the repository, starting only the craftar on PATH; uninstall and status complete it.

← All capabilities
F10 working

Registry and dashboard

problem · P2With a dozen workspaces there is no view of which is behind, drifted or on which Forge version.

$ craftar workspaces [--fetch] # every registered workspace, its status computed now$ craftar workspaces forget <dir> · craftar workspaces prune$ craftar ui [--port <n>] [--allow-writes] # a local dashboard on 127.0.0.1, read-only unless --allow-writes

A writing sync records its workspace in ~/.craftar/registry.json. craftar workspaces prints each one (profile, stack, targets, Forge, last sync) with a status computed on the spot: up to date, outdated, drift, no lock, missing or error, plus forge moved when the Forge's commit is no longer the lock's; it reads a remote Forge from the cache unless --fetch is given. CRAFTAR_NO_REGISTRY turns the registry off, for CI runners and throwaway checkouts. From the Forge's side, craftar forge impact lists every registered workspace that reads it (by its path, by a git remote, or through another clone) with what its next sync would do; a workspace synced on CI, on another machine or with the registry off is not in it. craftar ui serves these views as a local page in a browser: the table of craftar workspaces and, for a registered workspace, what status, diff and explain say about it and what recipes and ingredients say about its Forge. Without --allow-writes it is read-only: it writes nothing (the Forge cache is used as every reader uses it) and never runs sync: it prints the command to copy. It reaches no network either way: a remote Forge is read from the cached copy, and the page says so. Started with --allow-writes (typing the flag is the consent) the page also offers Sync for a workspace and Discard edit for one hand-edited file (drift or orphan-drift, from its open diff), each confirmed in the page: a sync that never passes --overwrite-drift (every hand edit and collision is skipped and listed) or a craftar drift discard of that one path. Neither is offered for .claude/settings.craftar.example.json or for a workspace whose Forge is remote, and either is refused when the workspace changed since the page showed it. It listens on 127.0.0.1 only (port 4747 by default; --port 0 takes a free one), answers GET only (with --allow-writes, also POST to its two writing routes, from its own address) and every answer that carries data needs a token generated for that run, carried in the address it prints: whoever holds that address can read what the reading commands print, so open it only in a browser of the same user on the same machine, exactly as printed. It takes no --workspace, and exits 1 when the registry is off or cannot be read. craftar ls stays the view of one workspace's resolution.

← All capabilities
F11 working

Craftar as an MCP server

problem · P1 · P2The agent itself should be able to ask what is pending, why a file exists and what the Forge offers without leaving the session: and say "run craftar sync" instead of editing a generated file.

$ craftar mcp [--workspace <dir>] [--fetch] [--allow-writes] # started by the AI coder, over stdin and stdout> /plugin marketplace add llima/craftar-cli · /plugin install craftar@craftar # in Claude Code: the /craftar skill and the reading tools$ craftar mcp allow-writes · craftar mcp revoke-writes # the consent, typed by a person in a terminal, per workspace

An MCP server the AI coder starts: registered in its own MCP configuration, or for a whole team as an ordinary mcp ingredient that sync writes like any other server. It serves the one workspace it was started in through seven tools that only read (status, diff, explain, recipes, ingredients, targets, doctor) each answering with the data its command prints, so the agent can say "run craftar sync" instead of editing a generated file. Without --allow-writes it writes nothing, no workspace file, lock, registry or Forge (the Forge cache is used as every reader uses it), reads no network unless started with --fetch, and never returns a hand-edited MCP configuration file; a result can carry the Forge's profile names, so register it only where that is acceptable. Started with --allow-writes, it also serves sync, add_recipe and remove_recipe. The flag grants nothing by itself: a writing call acts only in a workspace where a person typed craftar mcp allow-writes in a terminal, for the Forge, ref and profile the server is pinned to, the shell tools of Claude Code and Kiro give a program no terminal, so an agent cannot grant it through them (one that drives a real terminal could), and craftar mcp revoke-writes takes it back without a restart. The sync tool never overwrites or removes a hand edit: every drift, orphan-drift and collision is skipped and listed, for a person to resolve with drift discard or drift promote, and a remote Forge needs --fetch; add_recipe and remove_recipe never sync. A result carries what the commands print, never a file's content. Upgrade every machine to craftar 0.29.0 or later before adding it to a shared Forge, and to 0.35.0 or later before registering it with --allow-writes. In Claude Code, a plugin in the CLI's repository holds a /craftar skill and starts the craftar on PATH (install 0.40.1 or later) as craftar mcp with its reading tools only: never --allow-writes, never --fetch; in a project with no craftar.yaml it does not start. When craftar comes from a version manager's shim (asdf, volta, mise once trusted), a project's files can choose what runs when the server starts. Planned: an Agent Plugin.

← All capabilities
F12 working

Diagnostics

problem · P2Sync failures usually come from the environment: missing env vars, wrong Node, no Docker, unreachable Forge.

$ craftar doctor [--fetch] [--strict] [--json] # one line per finding, each with its fix$ craftar docs validate [path] [--fetch] [--strict] [--json] # the workspace's Markdown: links, leaked tool-call tags, encoding, the profile's docs: block

Checks this machine (Node ≥ 22, git, ~/.craftar, the registry, the Forge cache) and, inside a workspace, its configuration and Forge, the plan's warnings, declared parameters with no value (an error when a file the plan writes cites one, a warn when nothing does), the variables a written MCP server declares in authEnv or references as ${NAME} and the environment lacks (names only, never values), the lock, status and registration, and (in a workspace with a local service) Docker with its Compose plugin and the variables a service fragment needs that nothing sets (names only), and (when the profile declares templates) the templates block and, for a dotnet-new entry, the .NET SDK (and the planned AGENTS.md against its byte budget) and, under agents-md.primary, a CLAUDE.md, CLAUDE.local.md or .claude/CLAUDE.md (at the root or in a directory above it) that keeps Claude Code from reading AGENTS.md and the Kiro agents' hand-written resources, and, when the plan writes a hook's registration, a planned handler that is in no settings file Claude Code reads (at most a warn, so --strict exits 1 until craftar settings apply runs or the block is copied), and, when claude-code writes a skill as one file, that Claude Code (measured with 2.1.310) does not list it (a warn per skill, with the fix, so --strict exits 1; the file is still written), and, in a git repository, a generated file git ignores (generated-ignored, a warn, unless the path is listed under mayBeIgnored), and a hook craftar git-hooks wrote (git-hooks): and a path that leaves the workspace through a symbolic link (workspace-links, an error). Each finding is one line (ok, warn or error) with the command that fixes it. It only reports: nothing is written to a workspace, a lock or the registry, and a remote Forge is read from the cache unless --fetch is given. It exits 1 on an error, or on a warn under --strict; --json is a contract for tooling. craftar docs validate checks a workspace's Markdown under docs/, .claude/ and at its root, links that resolve inside the workspace, tool-call tags leaked outside code, files that are not UTF-8 text and, where the profile declares a docs: block, its categories, required frontmatter, dated file names and related: entries, one line per finding, naming a file, a line and a column, never a line of the file; it writes nothing and reads without the network unless --fetch, which concerns the Forge only.

← All capabilities
Targets

Capability matrix

what each target writescraftar targets prints this matrix from the CLI's own table, and sync skips by it: with a warning that names the ingredient, never silently. The five targets after the first three were validated on 2026-11-07 by opening each tool on a workspace craftar synced; opencode, new in 1.6.0, is mvp.

working claude-code · kiro · agents-md · cursor · copilot · agent-skills · codex · antigravity   mvp opencode
Ingredient typeclaude-codekiroagents-mdcursorcopilotagent-skillscodexantigravityopencode mvp
rulenative
.claude/rules/<name>.md
converted
.kiro/steering/<name>.md
converted
AGENTS.md
converted
.cursor/rules/<name>.mdc
converted
.github/instructions/<name>.instructions.md · .github/copilot-instructions.md
unsupportedunsupported
through agents-md
converted
.agents/rules/<name>.md
unsupported
through agents-md
agentnative
.claude/agents/<name>.md
converted
.kiro/agents/<name>.json
unsupportedconverted
.cursor/agents/<name>.md
converted
.github/agents/<name>.agent.md
unsupportedconverted
.codex/agents/<name>.toml
converted
.agents/agents/<name>.md
converted
.opencode/agents/<name>.md
commandnative
.claude/commands/<name>.md
converted
.kiro/steering/commands/<name>.md
unsupportedunsupported
through agent-skills
converted
.github/prompts/<name>.prompt.md
converted
.agents/skills/<name>/SKILL.md
unsupported
through agent-skills
unsupported
through agent-skills
converted
.opencode/commands/<name>.md
skillnative
.claude/skills/<name>/<file> · .claude/skills/<name>.md
converted
.kiro/skills/<name>/<file>
unsupportedunsupported
through agent-skills
unsupported
through agent-skills
converted
.agents/skills/<name>/<file>
unsupported
through agent-skills
unsupported
through agent-skills
unsupported
through agent-skills
mcpnative
.mcp.json · .claude/settings.craftar.example.json
native
.kiro/settings/mcp.json
unsupportedconverted
.cursor/mcp.json
converted
.vscode/mcp.json · .github/mcp.json
unsupportedunsupported
configure .codex/config.toml by hand
converted
.agents/mcp_config.json
converted
.opencode/opencode.json
scriptnative
.claude/scripts/<file>
unsupportedunsupportedunsupportedunsupportedunsupportedunsupportedunsupportedunsupported
steeringunsupportednative
.kiro/steering/<name>.md
unsupportedunsupportedunsupportedunsupportedunsupportedunsupportedunsupported
hooknative
.claude/hooks/<file> · .claude/settings.craftar.example.json
unsupportedunsupportedunsupported
through claude-code
unsupported
through claude-code
unsupportedconverted
.codex/hooks.json
converted
.agents/hooks.json
unsupported
serviceunsupportedunsupportedunsupportedunsupportedunsupportedunsupportedunsupportedunsupportedunsupported
native: written in the tool's own place for that type, as the Forge holds itconverted: written in another form (frontmatter or a banner added, references rewritten, another container format, or folded into a shared file)unsupported: not written; sync warns and names the ingredientthrough agent-skills / through agents-md / through claude-code: the tool reads that type from another target's file (for hooks, the .claude/settings.json that craftar settings apply writes); sync warns only when that target is off or the ingredient is not aimed at it, and never writes it twice.Measured in each tool: Codex reads .codex/agents/ only in a trusted project, and .codex/hooks.json only once its hooks were reviewed, an unreviewed hook is skipped silently; Antigravity run headless never applied a glob rule; .github/prompts/ and .vscode/mcp.json are VS Code's: the Copilot CLI reads commands from .agents/skills/ and MCP servers from .github/mcp.json.opencode is mvp: OpenCode 2.2.5 loaded its agents, commands, skills and MCP servers through its local service, with no model; it stays mvp until a model session uses those files. It needs OpenCode 2 or later, and its rules come through AGENTS.md.service is aimed at no AI coder: sync writes it under .craftar/ for no target.craftar targets also prints what each converted cell changes.
← The model
Reference

Model and terms

Forge
forge/ · one Git repo Repository holding every ingredient, recipe and profile. Versioned; workspaces point at it by local path or by git URL, with an optional branch, tag or full SHA; a remote Forge is read from a per-machine cache.
Ingredient
ingredients/rules/frontend-angular/ Smallest reusable unit: a rule, agent, command, skill, hook, MCP server, script, steering file or local service. A folder with an ingredient.yaml declaring type, targets, inclusion mode and globs. A service is aimed at no AI coder: sync writes it under .craftar/. Project templates are not an ingredient: a profile declares them.
Recipe
recipes/frontend-angular.yaml Named set of ingredients with parameters. Recipes extend others; stack recipes declare a slot so two front-end stacks cannot coexist.
Profile
profiles/team-a/profile.yaml One team: recipes, repositories, template feeds, PM tool, internal platform, and which AI coders that team uses, and, in a Forge at schema: 8, its own ingredients and recipes. Parameters and section overrides for the whole stack live here.
Workspace
craftar.yaml Local folder (one repo or a folder of repos) declaring profile, recipe add/remove (edited by craftar add recipe / remove recipe) and targets, plus a git-ignored craftar.local.yaml for per-developer overrides.
Target
claude-code · kiro · agents-md · cursor · copilot · agent-skills · codex · antigravity · opencode An AI coder to emit for. Eight targets working: claude-code, kiro, agents-md, cursor, copilot, agent-skills (the standard .agents/skills/ folder), codex and antigravity, and opencode, mvp. Warp, Kimi and other tools read the emitted AGENTS.md. Coverage is a table in the CLI: craftar targets prints it, per ingredient type: native, converted or unsupported. Sync skips an ingredient aimed at a target that the table marks unsupported, and warns, naming it: a fallback is planned.
Lockfile
craftar.lock The Forge's source, requested ref and commit (null when the Forge is not a git repository or has no commits yet), the resolved profile, recipes and targets, and a normalized content hash for every generated file; the lock is rewritten only when its own content changes. The only authority on what Craftar owns in a workspace.
Registry
~/.craftar/registry.json Every workspace a writing sync ran in on this machine: path, profile, Forge, recipes, targets, last sync. craftar workspaces lists them with a status computed on the spot; forget and prune remove entries, and CRAFTAR_NO_REGISTRY turns the registry off. craftar ui shows them as a local page on 127.0.0.1, read-only unless started with --allow-writes. craftar ls stays the view of one workspace's resolved recipes and ingredients.
← The model
Reference

Invariants in full

derived from the failure modesThe constraints Craftar is built against, in every command and every emitter. The one marked planned is a design commitment, not yet enforced by the code.

Never touch a file not in the lockfile
The lock is the complete list of managed outputs. Unknown files inside .claude/ or .kiro/ are preserved and never removed; one at a path the Forge would write is reported by status as a collision and never touched. Regeneration is per file. ← FM-1
Never hash raw bytes
Content hashes are computed after normalization: LF line endings, UTF-8, BOM stripped; JSON compared structurally. Claude Code and AGENTS.md outputs keep the EOL and BOM of the file they replace; Kiro output keeps the EOL and never writes a BOM. ← FM-2
Never leave an orphan on disk
An output whose ingredient left the resolved set is removed on the next sync and named in the summary, in every target. --check fails on orphans, and diff shows an orphan as the removal of the whole file (a hand-edited one as a header, since sync keeps it). ← FM-3
Never require a manual mirror
Everything a second target needs (steering, agent JSON, command copies) is emitted from the same ingredient. If two files must say the same thing, both are generated. ← FM-4
Never split a rule from its reviewer planned
An architecture rule and the reviewer agent for that architecture are meant to be one ingredient with two outputs, so they cannot disagree. Planned: today the rule and its reviewer are separate ingredients (rule/<name>, agent/<name>-reviewer) grouped by a stack recipe. ← FM-5
Never inline a volatile value
Model pins, MCP versions, ports and organisation names are declared once in the profile and injected as {{params}} wherever cited (in a schema 4 Forge, inside an MCP server too; at any schema, inside a hook's registration, for the keys the hook declares). A parameter an ingredient declares with no default, that a file the plan writes cites and no layer sets, stops sync: nothing is written until it has a value. ← FM-6
Never translate
Language is an attribute of the ingredient. The Forge may hold rules in one language and docs in another; Craftar emits text as written. ← FM-7
Never store a secret
An MCP ingredient declares authEnv: the environment variables its server needs, names only, never a value; an item shaped like a known token fails the load. With claude-code, sync writes .claude/settings.craftar.example.json, each declared name with an empty value; craftar settings apply adds a missing name, empty, to the developer's own untracked .claude/settings.json, and never overwrites or prints a value there. craftar doctor names the variables a written MCP server declares in authEnv or references as ${NAME} and the environment lacks, never their values; craftar import refuses ingredients holding secret-like values, and a forge: URL holding credentials is refused. In a schema 4 Forge, a value rendered into an MCP server that is a URL with credentials or a token of a known format fails the plan, and the message names the field, never the value: a check, not a guarantee: a token of an unknown format still passes. A literal token in a hook's registration fails the Forge load, and a credential substituted into one fails the plan. A Forge file that is not valid YAML is reported by an error code and a position at most, never by quoting the line; one its schema refuses is reported by key, never by a value it holds. ← FM-8
← Failure modes
AI llms.txt

This page was written by a language model, from the project's specification and the notes of the workspaces that motivated it, and reviewed by a person. It is also meant to be read by language models: every diagram carries a prose description, the metadata is structured, and a plain-text version lives at llms.txt.