Configuration
Reference for pair-cli's config.json, registry definitions, behavior modes, and target configuration.
pair-cli uses a config.json file to define how Knowledge Base content is distributed to your project. This file controls which registries exist, where they install, and how files are synchronized.
File Location
The config file is located at the root of the KB dataset:
Override with --config <path> on any command.
Resolution Order
More than one configuration can be in play at once: the CLI's own defaults, the KB you are installing, and your project. They are merged per registry, weakest first:
| Layer | Source | Wins over |
|---|---|---|
| 1. CLI defaults | config.json shipped with pair-cli | n/a |
| 2. Source KB declaration | pair.config.json at the root of the KB named by install --source | CLI defaults |
| 3. Project configuration | pair.config.json in the project being installed into | the source KB |
4. Explicit --config <path> | the file you name | everything |
The source declares, the consumer overrides. An external KB that declares
skills.prefix: "acme-kb" installs its skills under that prefix with nothing to copy
into your project, and if your project declares a skills.prefix of its own, yours
wins. Only the fields a layer actually states are overridden; the rest fall through.
Layer 3 is your project's pair.config.json only. A config.json in your project root is
not read as a Pair configuration: the name is too common to claim.
What layer 2 may say
The source KB is the one layer whose content you do not control: it is shipped by whoever publishes the KB. So it may describe its own content and namespacing, and nothing else. Exactly these registry fields are honoured:
| Field | What a source KB may state | Checked as |
|---|---|---|
source | a KB-relative path that stays inside the KB | absolute, ..-escaping, Windows-drive and control-character values are dropped, and so is a path that only resolves outside the KB, through a symlink |
exclude | which of its own files are narrowed out | array of strings |
flatten | whether its own directories collapse | boolean |
flattenDepth | how far they collapse | positive integer |
description | free text | non-empty string, no control character |
prefix | a single path segment | one containing /, \, .., or a control character is dropped |
include is not honoured, even though it looks like a content field: for a mirror
registry it controls which target folders are cleaned up when absent from the source (a WRITE decision, not a content one) so a declared include is dropped like targets.
Everything else in a source pair.config.json is ignored, not merged, targets,
target_path, behavior, working_path and any top-level key. A KB can never decide
where the install writes; that stays with the CLI defaults, your project, and --config.
Nor can it decide where the install reads. The bound is physical, not just on the name: each half is checked after every symlink on the path is followed:
- a declared
sourcemust resolve inside the KB; - every symlink met while copying must resolve inside that registry's own source
directory: the narrower bound. One resolving anywhere else is skipped, with a warning
naming it. One resolving back inside it is followed only when it points at a file: a
symlink to a directory is skipped too (also at a warning), because following one would
need a cycle guard,
self -> .is contained as well. Ship a directory of content as a real directory, not as a link to one.
So a KB cannot make pair-cli install copy files from elsewhere on your machine into your
repository, and a registry ships only what physically lives under it. A link like
.pair/knowledge/shared -> ../../shared-guidelines is not installed even though its
target is inside the KB: the target is outside the knowledge registry's source. Put
shared content under the registry's own source directory instead.
A field failing its check is dropped on its own: the layer beneath supplies the value
and the rest of the declaration still applies. If what remains is well-formed but still
does not add up (flatten: false over a registry that sets flattenDepth, say), the whole
declaration is dropped with a warning and your own resolution stands: a source's config
never aborts your install, and is never half-applied.
Four further guards apply to layer 2, and only to it:
- A malformed source
pair.config.jsonis ignored with a warning printed to the console (never partially applied) and installation continues on your project's resolution. "Malformed" also covers valid JSON that is not an object, or whoseasset_registriesis not an object. - A declaration whose fields are all dropped does not report itself as applied: the
Configuration: …line naming the KB is printed only when at least one declared field actually survived, and a KB that declared something and contributed nothing gets its own warning. - A registry the source declares that this CLI has no definition for (a newer KB read
by an older CLI) is not installed: it is reported as skipped, with the reason
declared by source, unknown to this CLI. - Layer 2 never introduces a registry. To adopt one the CLI does not know, declare it
in full in your own
pair.config.json(source,behavior,description,targets, …): a partial entry relying on the source for the rest fails validation, because the source's version of that registry was never merged beneath yours.
Layer 2 applies only to a named source (--source, --git, --url), and it applies to pair-cli install and pair-cli update alike: an update that did not read the declaration would re-install the same content under the CLI's default names, leaving both copies behind. The default KB path resolves exactly as before.
pair-cli package ships the KB's pair.config.json in the release ZIP, so a KB installed from its published artifact declares exactly what the same KB installed from a directory or a git clone declares.
Structure
Registry Fields
| Field | Type | Required | Description |
|---|---|---|---|
source | string | Yes | Source directory within the KB dataset |
behavior | string | Yes | Sync behavior: mirror, add, overwrite, skip |
description | string | Yes | Human-readable description shown by --list-targets |
include | string[] | No | Source-relative folder entries to mirror, matched by path segment, not globs (default: the whole source) |
exclude | string[] | No | Source-relative entries never installed: the whole subtree is skipped |
flatten | boolean | No | Flatten directory hierarchy into dash-separated names |
flattenDepth | number | No | Bound flattening to the registry's entry depth: the first N source segments are joined, anything deeper is preserved as a real sub-path. Requires flatten: true; must be a positive integer. Omitted = every separator is flattened |
prefix | string | No | Prefix to prepend to flattened directory names |
targets | Target[] | No | Multi-target distribution configuration |
exclude matches path segments, not string prefixes: "process/setup" skips process/setup and everything under it, and leaves a sibling process/setup-helper alone. It means never install, not uninstall, behavior: overwrite performs no cleanup, so an entry already present in the target from an earlier run stays there when it later becomes excluded.
Remove it by hand, or use behavior: mirror for a registry whose target should track the source exactly.
Behavior Modes
| Mode | Description | Use Case |
|---|---|---|
mirror | Overwrite all files, delete removed files | Knowledge layer (upstream content) |
add | Add new files only, never overwrite existing | Adoption layer (user decisions) |
overwrite | Overwrite existing files, add new ones, don't delete | Partial updates |
skip | Do nothing if target exists | One-time initialization |
Behavior Summary
mirror: Target becomes an exact copy of source. Missing files are deleted. Use for content that should always match upstream.add: Only creates files that don't exist yet. Never modifies or deletes. Use for user-owned content like adoption files.overwrite: Updates existing files and adds new ones, but doesn't delete. Use for incremental updates.skip: Completely skips the registry if target directory exists. Use for one-time setup.
Target Configuration
Each registry can distribute to multiple targets:
Target Modes
| Mode | Description |
|---|---|
canonical | Physical copy of files (primary target) |
symlink | Relative symlink to canonical target |
copy | Independent copy (used when symlinks aren't supported) |
Notes:
- Exactly one target must be
canonical symlinktargets create relative symlinks to the canonical copy- Windows falls back to
copymode (symlinks not reliably supported)
Transform Configuration
When flatten: true and prefix is set, directory names are transformed:
The transform also updates:
- YAML frontmatter values (e.g.,
name: verify-quality→name: pair-capability-verify-quality) - Markdown links between transformed files
- Skill cross-references (e.g.,
/verify-quality→/pair-capability-verify-quality)
Bounded Flattening (flattenDepth)
Without flattenDepth, every separator is flattened, so a sub-directory of an entry becomes a sibling entry of its own. Set flattenDepth to the number of source segments an entry occupies and only those are joined; anything deeper stays a real sub-path inside the entry:
Compare with the unbounded result for the same source: .claude/skills/pair-process-review-references/deep.md: a sibling directory outside the skill, with the skill's own ./references/deep.md link pointing at nothing.
Notes:
- Set it when your registry's entries own sub-directories (e.g. an Agent Skills
references/progressive-disclosure directory). - A directory deeper than
flattenDepthis treated as content, not an entry: it gets no prefix, no frontmatternamesync, and no skill-reference mapping. - Entries normally all sit at that depth, and a mixed-depth corpus is still workable: a shallower entry is fine while it owns no sub-directory (pair's own root-level
nextskill installs beside 39 two-segment ones), and aborts the copy only once it does. So does an entry deeper than the depth that nothing owns as content. Either way the copy fails with an explicit error before writing anything, instead of installing something broken.
Example: Skills Registry
Working Area Override
.pair/working/ (checkpoints, reports) is operational data, not knowledge. It is protected simply by never being a registry target: pair-cli install/pair-cli update only touch paths inside a configured registry, so a directory that is not a registry source or target is never created, modified, or deleted (D14). There is no runtime carve-out, instead, pair-cli validate-config rejects any config whose registry target would overlap the working area. See the KB Structure Reference for the full convention.
Override the default path with a top-level working_path field, a sibling of asset_registries, not nested inside it. The value must be a project-relative path (relative to the project root, like asset_registries targets); an absolute working_path is rejected by pair-cli validate-config:
When set, the override path (not .pair/working) is the reserved path that no registry target may overlap, checked by pair-cli validate-config.
Link Validation
A top-level link_validation field, a sibling of asset_registries, tunes how
pair-cli kb-validate treats relative markdown links whose target is missing:
optional_link_patterns marks matched-but-missing targets as warnings instead of errors, so a KB can be validated in isolation without disabling whole registries. Patterns merge (union, trimmed and deduplicated) with --optional-link-patterns, and --strict overrides them back to errors.
A pattern is matched against the target resolved relative to the KB root, and, only where the link as written is a genuine spelling of that same target (its ../ climb lands on the KB root) or the target leaves the KB tree, against the link as written; only the documented glob syntax is supported, anything else (brace expansion, extglob, leading !) is matched literally and therefore never matches.
Write patterns in the resolved form, as above: it is the same string whatever the depth of
the file the link is written in. A target that is a true sibling of the KB root resolves one
level up (../apps/**), and **/apps/** covers both anchorings. The written form
(../../apps/**) matches only links whose ../ climb lands exactly on the KB root, so it stops
matching the same target as soon as it is linked from a deeper file.
The section is read from the config of the KB being validated (<--path>/pair.config.json,
falling back to <--path>/config.json when there is no pair.config.json, the same config the
registries are resolved from) so validating pair-cli kb-validate --path ./my-kb uses
./my-kb/pair.config.json (or ./my-kb/config.json), not the config of the directory you run the
command in.
Full semantics and glob syntax: kb-validate: Optional link patterns.
Execution Engine (engine)
A top-level engine field, a sibling of asset_registries, names the agent process
pair-cli run drives:
id names the engine: pi, opencode, claude or codex. Two optional maps, keyed by engine
id, sit beside it:
bin: where THIS machine keeps an engine's executable, when PATH cannot find it ({"bin": {"pi": "/opt/pi/bin/pi"}}). Consulted first when the engine is resolved.model: the model an engine drives, run-wide ({"model": {"codex": "gpt-5.5"}}). Per-stage selection is the workflow profile's.
Anything else (an unknown id, a non-object block, an unknown field, a bin/model key that is not
an engine id, an empty value) is a validation error, never a silent fall-back to the default: a
typo that quietly ran a different engine would be indistinguishable from a working configuration.
The block is optional and delta-only. Precedence, strongest first:
| Level | Wins over | Notes |
|---|---|---|
--engine <id> | everything | Per-invocation choice |
engine.id in pair.config.json | the schema default | Committed project preference |
Schema default (claude) | n/a | A repository with no pair.config.json runs from this |
pair-cli run prints the resolved engine, the exact command line and the level it came from before
it executes anything.
Autonomy and project-trust approval are deliberately not configurable here. They are
per-invocation flags only (--autonomous, --approve-project-trust): a committed file must
never be able to grant an unattended run permission to skip confirmations or to execute code
from a repository the engine does not trust.
Workflow Profiles (workflowProfiles)
An optional top-level workflowProfiles field declares which engine, model, effort level and
context policy each delivery-cycle stage (prepare, validate, implement, green, verify,
contract, merge) uses, for both pair-cli run --card and the in-session pair-workflow-cycle
skill, through one shared resolver.
default: the profile used when no--profile/--workflow-configis passed.files: a glob or array of globs; each file is one profile, indexed by its ownname.inline: profiles declared directly, keyed by name. Wins over a file of the same name.
A profile is { name, description, defaults, stages, modelClasses } (name required; an inline
profile takes its key as name). A stage entry overrides only the fields it declares, falling back to
defaults, then to the KB default (the run's engine, its default model, default effort, fresh
context).
| Field | Values |
|---|---|
engine | default (the run's engine), pi, opencode, claude, codex. Spawned per stage by pair-cli; in a session only reported |
model | default (the engine's own), an explicit id, a class (cheap, balanced, frontier) mapped to an id by modelClasses, or by-tier (the card's risk:* label through ## Model Policy). A near-miss of a keyword is refused as a typo |
effort | default, low, medium, high, xhigh, max. In a session enforced on Codex, a prompt request on Claude; pair-cli sends it to claude (--effort) and codex (-c model_reasoning_effort; xhigh/max map to high); for pi and opencode it warns once and marks the stage ignored |
context | fresh, reuse. reuse only into prepare and green |
Resolution is once per run, in this order: --workflow-config (verbatim), then --profile, then workflowProfiles.default, then the KB default. Both flags work with --card, --root or --parallel (forwarded to every child card). The coordinator prints the resolved profile, its source and the per-stage table before the first dispatch; node .claude/skills/pair-workflow-cycle/scripts/workflow-profile.mjs resolve --root . [--profile <name>] [--tier risk:<tier>] prints the same table (table in its JSON) without running anything.
An unknown name is profile-unresolved; context: "reuse" on a stage the cycle's transition table forbids (validate, verify, …), an unknown stage or key, or a name collision is refused at load, never a silent fallback. The profile's name and hash are recorded in every handoff but are not part of what invalidates prior evidence.
No workflowProfiles field and no flag: nothing changes.
Worked example and recipe: Automated development, Autonomy and per-stage engine and model. Full reference: the KB's workflow-profiles.md (automation guidelines).
Validation
Validate your config with:
What Gets Validated
- Required fields present (
behavior,source,description) - Behavior values are valid (
mirror,add,overwrite,skip) - Target paths are accessible
- Transform config is valid (non-empty prefix, not combined with symlink mode;
flattenDeptha positive integer, and only alongsideflatten: true) - Source paths exist (if specified)
- Include patterns are valid arrays
- No registry's
targetoverlaps a reserved path, the working area (default or overriddenworking_path), andworking_pathis project-relative; see Working Area Override
Environment Variables
| Variable | Description | Default |
|---|---|---|
PAIR_KB_CACHE_DIR | Override the KB cache root; slots are created under it. Must be an ABSOLUTE path with no .. segments: the CLI rejects anything else, because slots are deleted recursively | ~/.pair/kb |
PAIR_KB_DEFAULT_URL | Override default GitHub release URL | GitHub latest release |
PAIR_GIT_TOKEN | Sets an origin-scoped http.extraheader (git >= 2.31) for https:// git clones of private repos, never http://, never in the URL. Also refuses any redirect while attached. | Not set |
PAIR_DIAG | Enable diagnostic logging (1 to enable) | 0 |
Related
- KB Structure Reference: Directory layout
- CLI Commands Reference:
validate-configcommand - Skill Management: How transforms affect skills