Written to the project root on a successful pharn init, and updated by pharn add, pharn remove and pharn update.
Source: pharn-config.ts and
install-archetype.ts.
PHARN skills in your project read this file at runtime (e.g. to discover the installed archetypes/capabilities and the pinned commit).
| Field | Type | Description | |
|---|---|---|---|
pharnVersion |
string | Version of the PHARN CLI that ran the install | |
skillsVersion |
string | The repo's SKILLS_VERSION at the installed commit |
|
repo |
string | Source repo (pharn-dev/pharn-oss) |
|
commit |
string | null | Pinned commit SHA of the install; null if the SHA was unavailable |
|
installedAt |
string | ISO timestamp of the install / last update | |
archetypes |
array | Detected project archetypes (ssr / backend / spa / lib) |
|
capabilities |
array | Installed capabilities, each { name, role, source? } — see below |
|
layout |
string | Install layout your files are at: flat or pharn (absent → flat) |
|
modules |
array | Always [] for an archetype install (the install unit is capabilities) |
|
models |
object | pharn-oss's per-stage model/effort block (Models) | |
seam |
object | Seam-resolution policy (seam-config.ts) |
isArchetypeConfig treats the presence of a capabilities array as the marker of an archetype install.
Any top-level key not on this page is yours, and every command keeps it — see Keys pharn does not own.
layout is written only by pharn init and pharn update, each recording the layout of the clone it
actually copied from. pharn add never writes the field — it
refuses a clone whose layout disagrees with the recorded one,
because it installs a single capability and cannot migrate the rest of your tree.
Each entry records how it got there, which decides who owns it on the next pharn update:
source |
Set by | What pharn update does with it |
|---|---|---|
auto |
pharn init, or pharn update when selected for your archetypes |
Owns it — drops it if your archetypes stop selecting it |
manual |
pharn add, or pharn update when inferring legacy provenance |
Preserves it while it still exists upstream; drops it (named) if it does not |
| (absent) | a CLI older than this field | Inferred once, on the next pharn update (see below) |
source is optional — a config written before the field existed simply omits it and still loads.
Absence is never read as a default: it means provenance unknown, and only pharn update may resolve
it, because that is the one command holding a fresh capability index. On the first update after
upgrading, a source-less entry is inferred once — in the resolved set → auto, outside it →
manual — and written back explicitly. That second half is a reconstruction, not a recovered fact:
such an entry was either added by hand, or auto-selected by an older index and since de-selected
upstream, and nothing offline distinguishes the two. Tagging it manual is the fail-safe direction —
it is then kept if the capability still exists upstream, and dropped (with a named report line) if it
does not. pharn remove warns about re-adds for a literal auto only, and stays silent on an absent
value rather than guess.
A source present but outside {auto, manual} is a hand-edit error: pharn reports it by name
(capabilities[2].source) and exits, rather than falling back to "run pharn init". Deleting the
field is a valid fix — the next update sets it.
Re-running
pharn initon an existing project rewrites this file from its own fields, and carries over whatpharn updatewould keep. Amanualentry upstream still ships is installed again and staysmanual. An entry of anysourcewhose capability upstream ships but this pharn cannot read is kept exactly as it was (and listed infrozenCapabilities). Amanualentry upstream no longer ships is dropped and named; an entry with nosourceis re-resolved from the archetypes like anautoone. Keys pharn does not own are copied across. Everything else pharn owns is written fresh.initwarns before overwritingpharn.config.jsonand defaults to No. Usepharn updateto refresh an existing install;initis for installing one.
A sibling file, pharn.records.json, holds a sha256 per installed file. It is
written by the same operations that write this config and is stamped with this file's
skillsVersion + commit; if the two disagree, pharn update treats the store as unavailable —
present files that differ are skipped (unverifiable), but missing files are still restored.
Re-run pharn update once both files agree, or pass --force to back up and overwrite differences.
The hash map lives there rather than here so this file stays small and hand-editable.
Note that skillsVersion / commit describe the last complete install: a pharn update that
skipped any file deliberately leaves them at their previous values (see update).
When the only skipped files were ones you edited (modified / unrecorded), it also writes
pendingSkillsVersion — the version it applied to everything else — which lets
pharn add run at that version; the next complete pharn update removes it. A
value that is not a x.y.z version is ignored.
frozenCapabilities lists (role:name, sorted) the installed capabilities the last pharn update
(or re-run pharn init) kept because it could not read them upstream. While it is non-empty, pharn update re-fetches even
at the same skills version, so they are re-checked on every run. A capability that can be read again
stays listed while any of its files had to be skipped (one you edited, say), so the next run checks it
again; the field is removed once none are left. If you pharn remove one, the next update drops it
from the list. A value that is not a list of
role:name keys is ignored.
{
"pharnVersion": "0.4.0",
"skillsVersion": "3.0.2",
"repo": "pharn-dev/pharn-oss",
"commit": "daa06788…",
"installedAt": "2026-06-11T00:00:00.000Z",
"archetypes": ["ssr", "backend"],
"capabilities": [
{ "name": "a11y", "role": "griller", "source": "auto" },
{ "name": "security", "role": "griller", "source": "auto" },
{ "name": "n-plus-one", "role": "lens", "source": "manual" }
],
"layout": "pharn",
"modules": [],
"models": { "stages": { "…": "…" } },
"seam": {
"resolutionOrder": ["official-skill", "pinned-docs", "model", "fetch", "ask"],
"modelConfidenceThreshold": "high",
"haltOnUnknown": true
}
}The models block is pharn-oss's, not this CLI's: pharn-oss owns its schema and its defaults —
every other field on this page is pharn's. It declares a model and an effort for each PHARN product
stage, and it is the source of truth that each /pharn-* command's static model: / effort:
frontmatter is held to.
What applies a model is the command frontmatter, not this block. Claude Code runs each
/pharn-*command on themodel:/effort:in that command's own frontmatter; nothing reads this block to pick one. pharn-oss's checker, installed with PHARN, holds the two equal:node pharn/floor/check-model-config.mjs agreement(.dev/floor/in the legacy flat layout). So editing the block is half a change: update the command frontmatter too, or the checker tells you. A green check means two files agree, never that a stage ran on that model — pharn-oss states the bounds in itsLIMITS.md§8.
pharn-oss's block, as of pharn-oss 6.22.0:
{
"models": {
"stages": {
"default": { "model": "sonnet", "effort": "high" },
"spec": { "model": "opus", "effort": "high" },
"plan": { "model": "opus", "effort": "high" },
"grill": { "model": "opus", "effort": "high" },
"build": { "model": "sonnet", "effort": "high" },
"regress": { "model": "sonnet", "effort": "high" },
"verify": { "model": "sonnet", "effort": "high" },
"ship": { "model": "sonnet", "effort": "high" },
"loop": { "model": "sonnet", "effort": "high" },
"review": { "model": "opus", "effort": "high" },
"memory-promote": { "model": "opus", "effort": "high" },
"ac-test": { "model": "opus", "effort": "high" }
}
}
}The rules are those of pharn-oss's checker (check-model-config.mjs validate):
| Rule | What pharn-oss accepts |
|---|---|
| Shape | models.stages maps a stage name to { "model": …, "effort": … } |
default |
Required, inside stages — what every stage without its own entry resolves to |
| Stage names | default, or a product stage: spec, plan, grill, build, regress, verify, ship, loop, review, memory-promote, ac-test |
model |
An alias — sonnet, opus, haiku, fable, inherit — or a full id matching claude-[a-z0-9][a-z0-9-]* |
effort |
low, medium, high, xhigh or max |
A config with no models block, or a block with no stages, declares nothing, and pharn-oss's
checker passes it by design.
pharn initcopies pharn-oss's block verbatim from the rootpharn.config.jsonof the commit it installs, and prints it resolved per stage. If pharn-oss ships no block,initwrites none — it never invents one. If pharn-oss ships a block this pharn cannot accept,initwrites none and says why (see below). A re-runinitreplaces your block with pharn-oss's, as it rewrites every other key pharn writes.pharn updatetreats the block like a file: pharn-oss's block replaces one pharn wrote, and one you changed is kept. It also converts the format earlier releases wrote. See update.pharn statusprints every product stage beside the model and effort it resolves to — marked(default)when the stage has no entry of its own — under the label above, and lists by name anything pharn-oss's rules reject. See status.add,removeandlistleave the block exactly as it is.
No command refuses to run over this block: pharn does not validate it when it loads the config,
because it is not pharn's schema. pharn checks it where it copies, converts or shows it, with a
copy of pharn-oss's rules. A test in the pharn repository runs pharn-oss's checker beside that
copy over a corpus of cases and fails on any difference. If pharn-oss's rules move ahead of the copy in your pharn
(a new stage, a new alias), pharn does not apply the newer block, names what it could not accept, and
upgrading pharn fixes it. pharn-oss can also require the upgrade with MIN_CLI (see
pharn is too old for the current pharn-oss).
Releases up to 0.6.0 wrote a block of their own: a top-level default, and model ids — opus-4-8,
sonnet-5, fable-5, haiku-4-5 — that neither Claude Code nor pharn-oss's checker accepts. The
first pharn update with 0.7.0 or later fixes it, even when your skills version is current: an
unedited block is replaced with pharn-oss's, and an edited one is converted and kept. See
update.
The seam block records how PHARN should resolve an unfamiliar integration point. It is written on
every fresh install and user-owned afterwards — pharn never migrates it — and it is validated
on every command, so a bad hand-edit fails loudly rather than being ignored. Source of truth:
seam-config.ts.
The installed default:
{
"seam": {
"resolutionOrder": ["official-skill", "pinned-docs", "model", "fetch", "ask"],
"modelConfidenceThreshold": "high",
"haltOnUnknown": true
}
}| Field | Type | Meaning |
|---|---|---|
resolutionOrder |
array | The steps to try, in order. Must end with ask. |
modelConfidenceThreshold |
string | How sure the model must be before its answer counts. Requires a model step. |
haltOnUnknown |
boolean | Stop rather than guess when nothing in the order resolves. |
Five hand-edits are rejected by name: an unknown sibling key, an unknown step, a duplicate step, a
resolutionOrder whose last entry is not ask, and a modelConfidenceThreshold set without a model
step in the order.
Every field on this page is pharn's to write, including the
legacy fields it no longer writes — models too,
though pharn-oss defines what goes inside it. Any other top-level key is yours: pharn does not
interpret or validate it, and every command that writes this file keeps it. Upstream PHARN documents
two that you add by hand:
| Key | Read by | What it sets |
|---|---|---|
testResults |
/pharn-test, and /pharn-verify's acceptance-criteria check |
The JSON report format of each test gate |
ship |
/pharn-ship (ship.requireAttestation) |
true: the ship stage asks for a named person's attestation |
{
"testResults": { "test": "vitest-json", "test:e2e": "playwright-json" },
"ship": { "requireAttestation": false }
}Without testResults, /pharn-loop stops with blocked: no-test-runner. The shape of both keys is
upstream's to define — see the pharn-oss README,
Per-test results. Because pharn does not
own these keys it does not check them, so a typo in one is not reported by any pharn command.
How each command keeps them:
pharn add,pharn removeandpharn updateedit this file in place, so a key they do not write stays where it is.pharn initwrites the file afresh from its own fields, then copies every key pharn does not own across from the config it replaces, unchanged, after its own. It does this for any file that parses as a JSON object, including one the other commands refuse: a config missing itsmodulesarray (the case where they tell you to runpharn init), or one with an invalidseamblock. A file that is not valid JSON carries nothing over. Move it aside first (troubleshooting), then copy your keys back.
init never carries over a key pharn owns. It writes pharnVersion, skillsVersion, repo,
commit, installedAt, archetypes, capabilities, layout and modules fresh. models becomes
pharn-oss's block again (or is left out, when pharn-oss ships none) and seam goes back to its
default, so a hand-edit in either does not survive a re-run init — unlike pharn update, which
keeps an edited models block.
pendingSkillsVersion, frozenCapabilities and the legacy fields below are dropped. (capabilities
is rewritten too, but the entries you added with pharn add are kept as manual — see the
init command.)
The schema is additive (P7): a pharn.config.json written by an older, module-based CLI still loads,
and its now-unused fields are preserved on read. They are still pharn's fields, though, so a re-run
pharn init drops them rather than carrying them over.
Two fields are nonetheless load-bearing, and deleting either makes the file unreadable: a config
without a string skillsVersion or without a modules array is treated as absent, and every command
answers "No pharn.config.json found. Run pharn init first." modules is always [] on an archetype
install, which makes it easy to mistake for removable — it is not.
| Field | Type | Note |
|---|---|---|
constitution |
string | Legacy constitution variant (gdpr-strict/standard/minimal) |
installedSkills |
array | Legacy per-technology skills, each { skill, from } |
stackAnswers |
object | Legacy wizard answers, questionId → value |
isMultiTenant |
bool | Legacy multi-tenancy answer; nothing writes or reads it today |
The module/manifest install path itself has been removed, so add / update / remove / list /
status no longer operate on a pre-archetype config — they exit with a message pointing you to re-run
pharn init.
| Command | Trigger | Prompt | If declined |
|---|---|---|---|
init |
any of the install's write targets already exists (the set includes pharn.config.json) |
lists the conflicting paths (at most 10, then "…and N more") and asks Continue and overwrite? — default no | Cancel install (exit 0); nothing is written |
add / update |
an archetype config is required | none — the config is updated in place | n/a |
A project with no conflicting path gets no prompt at all. The target set is derived from the
fetched clone's layout plus your resolved capability selection, and .claude/settings.json is never
overwritten — so it is excluded from the check. The init summary step
itemises what the set contains.
When pharn.config.json is itself one of the conflicting paths, the prompt also names the
skillsVersion that config currently records, so you can see which version you are about to replace.
That value is read from your local config only — never fetched — and if the file cannot be read or
does not carry a plain version string, the clause is omitted and the prompt is otherwise unchanged.
init reaches that prompt only in an interactive terminal: off a TTY it exits 1 before fetching
anything, and it deliberately has no --yes.
For the files PHARN installs (as opposed to this config), update never overwrites one you have
edited unless you pass --force — see the update decision table.
Both update --force and pharn add copy every file they are about to overwrite into
.pharn-backup/<timestamp>/ first, preserving its project-relative path.