Interactive setup wizard. Default command when you run pharn with no subcommand.
pharn init
# equivalent
pharninit detects your project's archetype(s) and installs the PHARN capabilities that apply to them. It fetches nothing you did not ask for: only the capabilities matching your project, plus the fixed product surfaces (commands, hooks, docs, contracts, pharn-core, floor), are copied. There is no module catalog and no manifest.json fetch — capabilities are the install unit.
The
--archetypeflag is a deprecated no-op kept for one release: archetype detection is now the default, sopharn init --archetypebehaves identically topharn init.
init always asks which capabilities to install, and — only when any of its write targets already
exist — whether to overwrite them, so it needs a terminal. Off a TTY (CI, a pipe, a script) it exits 1
with a usage error instead of prompting into a stream nobody is reading:
$ echo "" | pharn init # stderr shown inline
■ pharn init is interactive — run it in an interactive terminal. There is deliberately no --yes
for init: it confirms before overwriting existing files, and auto-confirming that in a pipeline
is what the prompt exists to prevent.
$ echo $?
1The refusal happens before the clone, so a non-interactive init costs no network round-trip and
leaves your project untouched.
There is deliberately no --yes for init — unlike pharn update, which has one. The
second of init's prompts is the destructive overwrite confirmation, and auto-confirming file overwrites
in a pipeline is precisely the hazard that prompt exists to prevent. pharn init --yes is therefore
refused (exit 1), not accepted and ignored — as is any other option init does not take, such as
--force or --json, and so is an extra positional (pharn init extra → Unexpected argument).
--archetype is init's only option, and it is the deprecated no-op above.
A directory with no .git still gets its own, more useful error first (see Prerequisites below) —
the interactivity check never masks it.
sequenceDiagram
participant User
participant CLI as pharn_init
participant Prereqs
participant Detect as detect_archetype
participant Fetch as fetch_pharn_oss
participant Resolve as resolve_capabilities
participant Summary
participant Conflict as write_target_check
participant Install
User->>CLI: pharn init
CLI->>Prereqs: git (.git present)
Prereqs-->>CLI: ok or exit
CLI->>Detect: package.json + file-tree signals
Detect-->>User: "Detected archetypes" note
CLI->>Fetch: download pharn-dev/pharn-oss tarball (codeload)
CLI->>Resolve: capability index vs detected archetypes
CLI->>Summary: capabilities selected + skipped (with reason)
Summary-->>User: install / cancel
CLI->>Conflict: which write targets already exist?
Conflict-->>User: overwrite warning (only if any) · default No
CLI->>Install: copy capabilities + product surfaces, write config
Install-->>User: next steps
- Archetype — a closed set describing what your project is:
ssr,backend,spa, orlib(the frameworkless base). Detection merges two untrusted-but-name-only fact sources — yourpackage.jsondependency names and file-tree structural signals (e.g. a.tsxfile →spa) — then applies the archetype rule once. It is deterministic: the same project always yields the same archetypes. A wholly signal-less project resolves tolib. - Capability — one griller or lens (an auditor PHARN ships). Each declares
applies: 'universal'(always selected) or a set of archetypes. A capability is selected iff it is universal or itsappliesset intersects your detected archetypes; otherwise it is skipped, with the reason shown.
init fetches pharn-dev/pharn-oss at main, so a capability can be in a shape your installed
pharn version does not understand yet. init skips that one capability and names it, before the
summary you act on, then installs everything else normally:
1 upstream capability could not be read and was SKIPPED — not installed:
griller:backwards-compat (pharn/pharn-pipeline/grillers) — missing its markdown backwards-compat/backwards-compat.md.
Nothing under that capability's directory is copied into your project, and it is not recorded in
pharn.config.json. Upgrading (npm install -g @pharn-dev/pharn@latest) usually resolves it.
Separately, if pharn-oss ships a root MIN_CLI file declaring a minimum pharn version newer than
yours, init refuses before any prompt or write, cleans up the clone, and exits 1 — see
pharn add.
Shows the PHARN logo and CLI version.
.gitpresent — checked up front, before anything else (universal, framework-agnostic). Hard-fails if absent.
Reads package.json dependency names and walks the project tree (bounded and symlink-safe, skipping dependencies, VCS metadata, and build/deploy caches — node_modules, .git, dist, build, out, coverage, storybook-static, .next, .nuxt, .svelte-kit, .astro, .turbo, .vercel, .cache, .parcel-cache, and non-JS trees — __pycache__, .yarn) for structural signals, then reduces both to an Archetype[]. Three more names are skipped only where they are another ecosystem's tree, because they are ordinary folder names in a JS project too: target/ beside a Cargo.toml, pom.xml or build.sbt; vendor/ beside a go.mod, composer.json or Gemfile; venv/ or .venv/ holding a pyvenv.cfg. Anywhere else they are scanned like any folder, so a route at app/target/route.ts is still detected. Skipping those trees costs the walk nothing, so a large framework cache cannot exhaust its bound and hide your real source; the tradeoff is that a signal file you hand-authored inside one of those directories is not seen. The detected set is shown in a "Detected archetypes" note. Only names are tested against fixed in-code allowlists — no discovered file body is read (other than package.json, which must be a regular file of at most 4 MiB — a leading UTF-8 BOM is ignored; anything else counts as no package.json) and no untrusted value is executed, interpolated, or logged.
If a proxy is configured in your environment, init warns first: pharn uses Node's global fetch,
which reads no proxy variable on any platform, so a proxy-only network fails as an unexplained timeout
unless you are told. Resolves the branch head via the GitHub API, then downloads that exact commit's tarball from codeload.github.com and extracts it into a temp dir. If the fetch fails — or the archive contains an entry pharn refuses to extract — the CLI exits; re-run with PHARN_DEBUG=1 for details. The temp clone is always cleaned up — on success, on error, on cancel, and on Ctrl-C or a SIGTERM mid-clone — and pharn keeps no download cache. An interrupted run also exits 130 (or 143 for SIGTERM) rather than reporting success.
If the fetched version declares a MIN_CLI newer than your CLI, init stops here with a named error
and writes nothing.
Parses the capability index from the fetched clone and selects the capabilities that apply to your archetypes (universal + archetype-matched), in the index's declared order. Skipped capabilities are kept with a reason (e.g. applies to [backend]; detected [ssr]).
Lists the selected capabilities (name, role, and why — universal or the matched archetype) and the skipped ones (with reason). Then:
| Action | Result |
|---|---|
| Yes, install | Copy the capabilities + product surfaces and write config |
| Cancel | Exit 0; nothing written |
After you choose install, init checks which of its actual write targets (the selected capability dirs, product pharn-* commands, .cjs hooks, pharn/features/README.md, the contracts, core and floor dirs, the trusted docs, pharn's LICENSE copy, and pharn.config.json) already exist in your project. If any do, it lists them (capped at 10, then "…and N more") and asks you to confirm before overwriting — default no. When pharn.config.json is one of them, the warning also names the skillsVersion your existing config records, so you can see which version you are about to replace (read locally, never fetched; the clause is simply omitted if that file cannot be read). If none do, there is no prompt (zero friction). .claude/settings.json is never overwritten, so it is excluded from the check.
Re-running init over an existing install. init overwrites every file it installs, so before it
does, it copies the ones you could lose to .pharn-backup/<timestamp>/ and prints that directory as soon
as it is created. Which files those are is decided the way pharn update decides what to
skip: against the hashes in pharn.records.json that the previous install wrote. The prompt lists these
files first, marked, and says how many there are of each kind:
| Marked | Meaning | Backed up |
|---|---|---|
(edited) |
The file changed since pharn wrote it — your edit | Yes |
(no pharn record) |
It differs from upstream and the records have no entry for it | Yes |
(differs from upstream) |
It differs from upstream, and there is no usable pharn.records.json to tell your edits from upstream changes, so every difference counts |
Yes |
| (not marked) | Byte-identical to upstream, or still exactly what pharn wrote (upstream simply moved on) — a clean upgrade | No |
pharn.records.json counts as usable only when it matches the skillsVersion and commit of the
config being replaced, as for update. Records are kept by path, so a file of yours at a path the
previous install did not write — for example your own file at a path a newer pharn now installs to —
has no record and is backed up, marked (no pharn record). A change of layout (flat → pharn/)
installs to new paths and leaves the old copies where they are (init never deletes), so it backs up
nothing extra. pharn's own LICENSE copy (PHARN-LICENSE / pharn/LICENSE) is compared with
upstream's LICENSE, like every other file with its source. The marks are a preview: init checks again just before it copies,
under the project lock, and that check decides what is backed up — so an edit you make while the prompt
is open is still saved.
Capabilities you added by hand with pharn add (source: "manual" in pharn.config.json) are kept:
init installs them again and records them as manual — also when your archetypes now select them
too — as long as upstream still ships them. The summary lists them as "added by hand". One that upstream
no longer ships is dropped from pharn.config.json and named (its files stay on disk). A capability
upstream still ships but this pharn cannot read
is left as it is, however it was added: its config entry, files and records are untouched, and it is
listed in frozenCapabilities so the next pharn update re-checks it. Top-level keys in
pharn.config.json that pharn does not own, such as upstream's testResults and ship, are copied
across unchanged — see Keys pharn does not own.
An unreadable or invalid existing config never blocks init. Capabilities are carried over only from a
config the other commands would accept, and your own keys from any config that parses as a JSON object;
a config that is not valid JSON carries nothing over. If pharn.config.json changes while init waits at
its prompts (another pharn command wrote it), init refuses and writes nothing; re-run it. The target set is derived from the fetched clone's layout + your resolved selection (lib/install-manifest.ts), so it is exact — not a git-history heuristic.
When init refuses to install. After you choose install, and before it asks to overwrite
anything, init checks every path it is about to write and stops, naming each problem, if the copy
could not finish: a symbolic link on the way (writing through a linked directory would put files
outside your project, and a linked file would be replaced by pharn's copy), or an entry of the wrong
kind (a directory where pharn writes a file, or a file where it needs a directory — pharn.config.json
and pharn.records.json included). So you are never asked Continue and overwrite? for an install
that could not finish. The same check runs again under the project lock, before anything is written —
the backup included — so a problem that appears while the prompt is open is still refused. A refused
install writes nothing: no files, no config, no records and no .pharn-backup/. A symbolic link at
.claude/settings.json is fine while it points at an existing file — init never writes an existing
settings file — and is refused only when its target does not exist. See
Something in your project is in the way.
| Action | Behavior |
|---|---|
| Copy capabilities | Each selected griller/lens dir (with its evals/) → the mirrored project path |
| Copy product surfaces | pharn-*.md commands (not pharn-dev-*), .cjs hooks, the trusted docs, pharn/features/README.md, and the contracts, pharn-core and floor dirs — each at the fetched layout (minus test files and test-fixtures/) |
| Preserve settings | An existing .claude/settings.json is never overwritten (a note tells you to wire the hooks by hand if needed) |
| Mirror the layout | Whichever layout the fetched clone uses is mirrored verbatim; the CLI never rewrites copied file contents. Today that is pharn/pharn-contracts/, pharn/pharn-core/, pharn/floor/; the legacy flat layout is pharn-contracts/, pharn-core/, .dev/floor/ |
| Pin commit SHA | Best-effort (the SHA the tree was pinned to; null if unavailable) |
Write pharn.config.json |
pharnVersion, skillsVersion (from SKILLS_VERSION), repo, commit, installedAt, archetypes, capabilities (source: "auto", or "manual" for a kept pharn add), layout, models, seam, modules: [], frozenCapabilities (see above) |
Copy the models block |
pharn-oss's own, verbatim, from its root pharn.config.json; none when pharn-oss ships none, and none — with the reason — when it fails pharn-oss's rules as this pharn knows them (Models) |
Write pharn.records.json |
A sha256 of every file the install wrote, so pharn update can keep your later edits (reference) |
The install also copies pharn-oss's Apache-2.0 LICENSE — to pharn/LICENSE, or PHARN-LICENSE at
the root in the legacy flat layout. The destination is deliberately not a plain root LICENSE:
that file is yours, and pharn never overwrites it. The copy exists so that a repo you commit and
publish carries the license grant for the ~450 Apache-2.0 files pharn put in it.
The install copies pharn-oss's canonical CONSTITUTION.md verbatim — there is no privacy-posture / constitution-variant question in the archetype flow. Only capability contents are copied; the CLI never executes or parses them (your Claude Code runs them later).
On success, the CLI reports the capability count, prints the models block it copied resolved per stage — under the label that Claude Code applies each command's own frontmatter, which the block is the source of truth for — and suggests opening Claude Code and running /pharn-spec — intent capture for your first feature, which feeds /pharn-plan.
init takes the project lock (.pharn.lock) after both of its prompts and holds it across the
write, releasing it in the same step that disposes of the temp clone. A second pharn writer refuses
with a named message and exit 1 rather than queueing; pharn list and pharn status are never
blocked by it.
Two consequences are deliberate. Because the lock is taken late, a second writer racing pharn init
still pays for the full download before being refused — accepted, because the alternative is holding
the lock across an unanswered human prompt. And because both prompts sit inside the run, a walked-away
init can block other writers until the six-hour staleness window expires. See
Another pharn process is running.
init always writes an archetype config, and every command is archetype-only. A pre-archetype module-based pharn.config.json (one with modules[] but no capabilities[], from a much older release) is no longer supported: add, remove, list, update, and status detect it up front and exit with a message to re-run pharn init — there is no module/manifest fallback (live pharn-oss ships no manifest.json). The config schema is additive, so a legacy config's now-unused fields (modules, constitution, stackAnswers, installedSkills) still parse; only the absence of capabilities[] triggers the rejection.
A config that is present but invalid — a malformed seam block, an out-of-enum
capabilities[].source, or JSON that does not parse — is a different case with its own named error and
exit 1. (A models block is never one: it is pharn-oss's, and no command refuses to run over it.) It deliberately does not say "run pharn init", because that would tell you to overwrite
the file you need to repair. See
A command rejects an invalid config.