kitbash / docs / config

Project config

One file configures a repo: kitbash.toml. It has two tables — which agents to compile for, and which skills anyone is allowed to install. Everything else Kitbash owns on disk is derived from it.

What kitbash init creates

Run from the root of a repo:

kitbash init
created kitbash.toml and .kitbash/skills/
next: kitbash install <gh:owner/repo | owner/repo | file:path>, then kitbash compile

Two things: the config file, and the empty directory installed skills land in. If kitbash.toml already exists, init prints kitbash.toml already exists — nothing to do and changes nothing — it is safe to re-run and it will never overwrite your config.

Everything in the file it writes is commented out apart from the [project] header, so a fresh config changes nothing. Every key is optional:

# kitbash project configuration — https://github.com/singhharsh1708/kitbash
[project]
# Adapters to compile for. Omit to autodetect (.claude/, .cursor/, AGENTS.md floor).
# targets = ["claude-code", "cursor", "agentsmd"]

# Install policy (org allowlist). Enforced at install and rechecked by doctor.
# [policy]
# allow_sources = ["gh:your-org/*"]  # globs; matched against gh:owner/repo[/path][@ref] or file:/abs/path
# deny_network = true                # refuse skills declaring network permission
# deny_write = true                  # refuse skills declaring write permission
# max_budget = 6000                  # refuse skills with a larger context budget
# deny_remote_exec = false           # opt OUT of the download-and-execute body lint (default: on)

init does not create kitbash.lock. The lockfile appears on the first successful kitbash install.

Kitbash reads kitbash.toml from the current working directory only. There is no upward search for a config in a parent directory, so run commands from the repo root.

The [project] table

One key: targets.

targets

An array of adapter ids. Omit the key and Kitbash autodetects — every adapter whose detection probe finds evidence of that agent in the repo. Set it and detection is bypassed entirely: the listed targets are compiled whether or not the agent's directory exists.

[project]
targets = ["claude-code", "cursor", "agentsmd"]

The nine valid ids, and what autodetection looks for:

idDetected when the repo contains
claude-code.claude/
cursor.cursor/
agents.agents/ or .codex/
copilot.github/
cline.clinerules/
windsurf.devin/ or .windsurf/
geminiGEMINI.md or .gemini/
aiderCONVENTIONS.md or .aider.conf.yml
agentsmdalways — it is the floor

An unrecognised id is a hard error, not a warning. kitbash compile writes nothing and exits 1:

unknown target(s) in kitbash.toml: claude (known: claude-code, cursor,
agents, copilot, cline, windsurf, gemini, aider, agentsmd)

That is deliberate. A typo'd target silently dropping an agent from every compile is worse than a failed build.

Note that kitbash doctor reports detection, not configuration — its "detected targets" list runs the probes regardless of what targets says. If the two disagree, targets is what compile obeys.

What each adapter emits, which capabilities it supports, and whether it loads lazily or eagerly is covered in adapters & targets.

The [policy] table

The org-allowlist layer: which sources may be installed, and what an installed skill is allowed to declare. Omit the table and there is no policy — every source is permitted.

allow_sources

An array of glob patterns. An empty array, or a missing key, means no source restriction at all.

The glob is minimal: * spans any run of characters including /, every other character is literal, and the pattern must match the whole string — it is anchored at both ends. There is no **, no ?, no brace expansion.

Each pattern is tested against two strings, and matching either one passes:

  • the raw source exactly as typed on the command line;
  • its canonical formgh:owner/repo[/path][@ref] for GitHub sources, or file: followed by the absolute resolved path for local ones.

So a bare acme/tools canonicalises to gh:acme/tools and is caught by "gh:acme/*" even though the user never typed the prefix. A local file:./skills/foo canonicalises to file:/Users/you/repo/skills/foo — absolute, machine-specific. Matching local paths by glob is possible but rarely portable; prefer allowing GitHub sources.

PatternMatchesDoes not match
gh:acme/*gh:acme/tools, gh:acme/tools/skills/lint, gh:acme/tools@v2gh:acme-labs/tools
gh:acme/toolsexactly that sourcegh:acme/tools/skills/lint, gh:acme/tools@v2
gh:acme/tools/*gh:acme/tools/skills/lintgh:acme/tools (no trailing segment)
*everything

Because * crosses /, pinning a ref needs care: gh:acme/* also admits gh:acme/tools@some-branch. If you want a specific ref, write the whole source out with no wildcard.

deny_network and deny_write

Booleans, default off. They gate what the skill's manifest declares under [permissions]:

  • deny_network = true refuses any skill with permissions.network = true.
  • deny_write = true refuses any skill with permissions.write = true.

Only the literal boolean true enables them. deny_network = "true" — a string — is read as off.

max_budget

An integer. A skill is refused when its declared context.budget is strictly greater than this value. It must be a number; a quoted value is ignored. This is a ceiling on what a skill may cost when active, not on your repo's total — kitbash doctor reports the repo-wide standing and worst-case active totals separately.

deny_remote_exec

Unlike the keys above, this one defaults to on — the remote-exec safety lint blocks install with or without a [policy] table. Setting deny_remote_exec = false is the deliberate opt-out: it lets a trusted internal skill carry a curl … | sh bootstrap in its body without being blocked. It exempts only remote-exec; the visible-text and dynamic-context lints stay in force. See trust & review for the full list.

Where policy is enforced

Two places, both non-negotiable.

At install. The review block prints first, then policy runs, then the confirmation prompt — so a violation blocks before you are ever asked. --yes skips the prompt, not the policy:

review: docfetch@1.2.0 — Pulls API docs into the review context
  budget 2000 tokens · standing 90 · lazy disclosure · mode skill
  permissions: tools [read, bash:curl *] · network YES · write no
  ✗ policy: docfetch declares network permission and deny_network = true
blocked by [policy] in kitbash.toml.

The command exits 1 and nothing is written to disk.

At doctor. kitbash doctor rechecks every installed skill against the current policy, using the source recorded in kitbash.lock. That catches the two cases install-time enforcement structurally cannot: skills installed before the policy was written, and skills copied into .kitbash/skills/ by hand. Violations print the same ✗ policy: lines and doctor exits 1.

Unknown keys are silent. Kitbash reads the keys it knows and ignores the rest — there is no schema check on kitbash.toml. A misspelled allow_source (singular) does not error; it just means no allowlist is in force. After editing [policy], run kitbash doctor and confirm it prints policy: ok. If the [policy] table is absent that line does not appear at all.

A complete kitbash.toml

# kitbash project configuration

[project]
# Compile only for these agents. Omit the key to autodetect from the repo.
# Listing a target compiles it whether or not its directory exists.
targets = ["claude-code", "cursor", "agentsmd"]

[policy]
# Sources anyone may install from. Anchored globs; * spans any characters,
# including "/". Matched against the raw source and its canonical form
# (gh:owner/repo[/path][@ref] or file:/abs/path). Omit for no restriction.
allow_sources = ["gh:acme/*", "gh:singhharsh1708/kitbash/examples/skills/*"]

# Refuse skills declaring permissions.network = true.
deny_network = true

# Refuse skills declaring permissions.write = true.
deny_write = true

# Refuse skills whose context.budget exceeds this.
max_budget = 6000

Kitbash's TOML reader covers the subset manifests need: tables, arrays of tables, bare keys, basic strings, integers, floats, booleans, single-line arrays, and # comments. Multi-line arrays and inline tables are not supported — keep each array on one line.

Files Kitbash manages

Three kinds, with different rules. You edit the first, never edit the second, and only edit around the third.

.kitbash/skills/

Where kitbash install copies each skill — one directory per skill, named after the skill. This is the source of truth for compilation: every command that operates on "installed skills" reads this directory, loading each subdirectory that contains a SKILL.md and skipping the rest, so an aborted install or a stray folder does not break every subsequent command.

Commit it. Vendoring the skill body is the point — your team gets byte-identical instructions the way they get byte-identical dependencies, and the lockfile can prove it.

kitbash.lock

Written on install and removal, sorted by skill name, one [[skill]] block each:

# generated by kitbash — do not edit

[[skill]]
name = "prereview"
version = "0.1.0"
source = "gh:singhharsh1708/kitbash/examples/skills/prereview"
integrity = "sha256-…"

integrity is a SHA-256 over the whole skill directory: every file's relative path and contents, in a deterministic order. Paths are Unicode-NFC-normalised and sorted by binary code-unit order, and text files have CRLF normalised to LF, so the same skill hashes identically on macOS, Linux, and a Windows checkout with core.autocrlf. Files containing a NUL byte are treated as binary and hashed verbatim.

Hashes are the truth; the version string is human convenience.

Drift detection. kitbash doctor rehashes each installed directory and compares:

ConditionReportExit
Hash differs from the lock entry✗ <name>: integrity drift — installed files differ from kitbash.lock1
Installed but absent from the lock✗ <name>: installed but not pinned in kitbash.lock — reinstall to pin it.1
Skills installed, no lockfile at all✗ N skill(s) installed but kitbash.lock is missing — nothing is pinned. Reinstall to regenerate it.1
In the lock but not installed⚠ <name>: in kitbash.lock but not installed0
Everything matcheslock integrity: ok0

Drift means someone edited a vendored skill in place, or a merge mangled it. Either way the instructions your agents are running are no longer the instructions that were reviewed at install. Commit the lockfile, and run kitbash doctor in CI so the diff is caught by a machine rather than trusted by a human.

Generated agent outputs

kitbash compile writes one native artifact per skill per target. Every generated file carries a marker on its first content line:

<!-- generated by kitbash — do not edit; source: .kitbash/skills/prereview @ 0.1.0 -->

The marker is not decoration. It is how Kitbash decides what it is allowed to delete: on each compile, the managed directories below are scanned, and a file is removed only if it bears the marker and was not written by the current run. Anything you wrote by hand into those directories has no marker and is never touched.

TargetWrites
claude-code.claude/skills/<name>/SKILL.md, plus .claude/commands/<cmd>.md per declared trigger command
cursor.cursor/rules/<name>.mdc
agents.agents/skills/<name>/SKILL.md
copilot.github/skills/<name>/SKILL.md
cline.clinerules/<name>.md
windsurf.devin/rules/<name>.md, or .windsurf/rules/<name>.md when there is no .devin/
gemini.gemini/skills/<name>/SKILL.md
aiderCONVENTIONS.md (merged section)
agentsmdAGENTS.md (merged section)

Removing a skill and recompiling prunes its outputs, reported as ✂ removed .cursor/rules/prereview.mdc (stale). Renaming a trigger command prunes the old slash command the same way.

Marker-merged shared files

AGENTS.md and CONVENTIONS.md are files you already own and already write in. Kitbash cannot overwrite them, so it does not: each skill gets a delimited section, and only the bytes between its markers are ever replaced.

# Contributing

Build with `make`. Run the integration suite before you open a PR.
Everything above and below the markers is yours — kitbash never rewrites it.

<!-- kitbash:begin prereview -->
<!-- generated by kitbash — do not edit; source: .kitbash/skills/prereview @ 0.1.0 -->

## Skill: prereview

Reviews a diff against the team's actual standards.
…
<!-- kitbash:end prereview -->

On each compile the section is replaced in place if its markers are already present, and appended after your existing content if not. Sections belonging to skills that are no longer installed are dropped. If nothing wrote to a managed shared file at all during a compile — AGENTS.md, GEMINI.md or CONVENTIONS.md — every generated section in it is stale by definition and all of them are pruned, printing ✂ pruned stale section(s) from AGENTS.md. That is what makes removing the last skill actually clean up, and what removes the GEMINI.md sections an older Kitbash wrote before gemini moved to .gemini/skills/.

Do not edit inside the markers. Your changes are overwritten on the next compile, and — worse — the file no longer matches the reviewed skill while looking as though it does. Edit the skill in .kitbash/skills/ and recompile, or write your own prose outside the markers.

Both of these targets are eager: whatever sits between the markers is in the agent's context on every single session. That is the cost the benchmark measures, and why compile warns when a skill authored to lazy-load lands on one of them.

What to commit, what to ignore

PathVerdictWhy
kitbash.tomlCommitThe targets and the install policy are team decisions, not local ones.
.kitbash/skills/CommitThe reviewed skill body. Vendored on purpose.
kitbash.lockCommitWithout it nothing is pinned and doctor fails. It is what makes drift detectable.
AGENTS.md, CONVENTIONS.mdCommitMostly your own content; the generated sections are a subset.
.claude/, .cursor/, .agents/, .github/skills/, .gemini/skills/, .clinerules/, .devin/, .windsurf/EitherFully derived from the two above. Commit them and a fresh clone works with no build step; gitignore them and run kitbash compile in CI or a post-checkout hook. Pick one and be consistent — half-committed generated output produces confusing diffs.

Whichever you pick, the invariant is the same: kitbash.toml plus .kitbash/skills/ plus kitbash.lock is enough to reproduce every generated file exactly.

Where to go next