Skip to main content

Usage

Reads facets.json, materializes every facet declared there, and writes facets.lock with the exact resolved versions and integrity hashes. Use it after a clone, after pulling manifest changes, or to restore files someone edited by hand. Nothing is written until every facet resolves, verifies, and composes without a collision, so a run that fails or is declined before that point leaves the project byte-identical. If a failure happens after writing starts, the run rolls back what it wrote and names any file it could not return to its previous state. See when rollback cannot finish. facet install takes no positional arguments. To add a facet, use facet add.

Examples

Flags

boolean
Show detailed step output on stderr. Declaration contents never appear here. See Where declarations appear.
boolean
Approve the MCP configuration this run would write, without prompting. See Accept MCP.
boolean
Treat the lockfile as the source of truth and fail on drift. See Frozen lockfile.

Exit codes

On failure, stderr carries three lines: error:, a code= token for log searching, and a fix: action. Reports for collisions, MCP approval, and unsupported adapters print above that block, so fix: stays last.

Output

The assets written counter covers text assets only, and MCP servers are counted beside it, so a facet that only declares servers still reports real work. Server configuration is reported as added, updated, repaired, unchanged, or removed, and a line gains (took over an existing entry) when it adopted or replaced an entry this project did not create. Below the counts, the summary names every contribution whose materialized name differs from the published one:
Omitted contributions are never counted as written.

Outcomes

The summary classifies each facet by what happened on disk.
new
The facet was not in the previous lockfile.
state change
The facet was locked at a different version, or its materialization changed. Aliasing or omitting a text asset or server changes what the project materializes, so it counts as an update rather than a repair.
self-heal
Same version and intent, but a file was missing or had drifted from the lockfile content. A skill is repaired as a whole bundle, so one drifted companion pulls its primary with it. A drifted MCP entry is rewritten the same way.
no-op
Same version, same intent, everything already in its desired state. Matching bytes are adopted without a rewrite.
cleanup
The facet is no longer declared and this machine’s records tracked it. Obsolete files and owned server entries are deleted; anything another declared facet still claims is kept.
records only
The facet is no longer declared, and only the lockfile ever recorded it, which happens when you pull a teammate’s lockfile and never install. The declaration is dropped, but nothing on disk is deleted. Remove those files yourself.
The N removed count covers facets, not the assets they contributed. An untracked removal deletes nothing on disk.

Details

Name collisions

Two facets cannot materialize under the same name. Skills and commands share a namespace, agents have their own, and servers occupy a third. Collisions are also scoped, so a project-scoped text asset never contests a user-scoped one. See Namespaces. Two facets declaring the same server identically is not a collision. They agree, one entry is configured, and both are recorded as claimants. Collisions are detected before anything is written, so the run stops with the project untouched. In a terminal, the install pauses and opens a resolver. Each contested contribution takes one choice: Move between rows with ↑↓, choose with ←→, and apply with Enter. Each claimant shows ✕ unresolved, ⚠ conflict, or ✓ resolved. A contested server also shows its declaration summary and a short fingerprint, so you can tell two same-named servers apart. Confirmation unlocks when every group is resolved. Esc cancels the whole install without writing anything. Only one claimant has to change. Your choices are saved to facets.json:
facets.json
A servers group needs "manifestVersion": 0.2, which a successful non-frozen install writes for you. Without a terminal, nothing is prompted. Every group and claimant is printed with the exact facets.json location and copy-pasteable snippets. No winner is chosen and no name is invented: the placeholder is literally choose-a-name.

Frozen lockfile

--frozen-lockfile makes the lockfile authoritative. Nothing is re-resolved, the lockfile is never written, and every facet must reproduce its locked integrity. The lockfile’s recorded materialization intent must match facets.json. Frozen mode never prompts, even in a terminal, because it reproduces recorded intent rather than collecting new decisions. A collision fails, and MCP configuration needing approval fails unless you also pass --accept-mcp. A 0.2 lockfile has no field for a text-asset disposition, so a frozen run fails if facets.json declares one. Run a plain facet install once to migrate the lockfile. A servers override never causes this, because no lockfile version records server intent. Frozen mode reads manifestVersion 0.2, 0.1, and unversioned documents and migrates none of them. A stale servers override is reported as drift and is not removed.
Use this in CI to prove facets.json and facets.lock agree, the same way --frozen-lockfile works in npm and bun.

Cache

Resolved content is cached under $FACET_DIR/cache/, so repeat installs of the same version skip the network. Cache hits are re-verified on every use. See integrity.

MCP servers

A facet that declares servers contributes configuration to every selected adapter, written into that tool’s own project file. Only project-scoped files are touched. See where configuration lands. Installing a server configures it. Facets never downloads, launches, connects to, health-checks, or authenticates to it.

Accept MCP

--accept-mcp is the only non-interactive way to accept MCP configuration. It approves the complete set the run reports, including takeovers of existing entries. It does not authorize a text-asset takeover and does not resolve a name collision. The same flag exists on facet add, facet update, and facet remove.

Approving a declaration

In a terminal the install pauses on one screen listing every declaration awaiting approval, its claimant facets, and its exact command, arguments, and environment assignments or URL. Decline is selected by default, and approval covers the whole displayed set. Approval is stored per machine, keyed by the effective server name and the declaration’s content:
  • An already-approved declaration is not asked about again, including when a second facet declares it identically.
  • A changed command, argument order, environment value, transport, URL, or effective name asks again.
  • A run that fails or rolls back banks nothing.
  • Machine records written before MCP support carry no approvals, so the first install after upgrading asks once.
Approval never travels with your project. It is machine-local rather than recorded in facets.json or facets.lock, so a committed file cannot claim you consented to running something.

In CI and other non-interactive runs

Without a terminal, an unapproved declaration fails before anything is written and prints every server, its claimants, its full declaration, and the exact facets.json edit that would refuse it. Frozen mode fails the same way even on a terminal, because it never collects a new decision. Re-run with --accept-mcp to approve everything shown, or omit what you do not want:
facets.json

Taking over an existing entry

If a desired server name already has an entry this project did not create, it is disclosed on the same approval screen, before any write. Each line names the adapter and says whether the entry already matches and would be adopted, or differs and would be replaced. An equivalent entry is adopted without rewriting the document. Declining costs nothing, because no write has happened yet.

Taking over an existing file

Text assets have a separate prompt that appears mid-write, when materialization reaches an occupied destination this machine does not own. It names the adapter, scope, type, and effective name. Continue is the default here, because that is what a non-interactive run already does and because replacing a file is less consequential than authorizing a command. Cancelling rolls back everything the run has already written and exits non-zero. --accept-mcp never answers this prompt.

Adapters that cannot configure MCP servers

When the project has active declarations and a selected adapter declares no MCP support, the run fails before approval and before any write, naming every such adapter. That adapter declares no MCP support as a deliberate answer, so a newer release will not change it. Omit every affected server, or remove the adapter. With no active declarations left, an adapter without MCP support installs normally.

Where declarations appear

A declaration’s command, arguments, environment values, and URL appear on exactly two surfaces: the interactive approval screen and the non-interactive report that asks you to approve. They never appear in --verbose output or success summaries, which name servers by identity only. That keeps declaration contents out of scrollback and CI logs. On both surfaces values are quoted and escaped, such as "srv" "--root" "/w" and "TOKEN"="a b". Quoting keeps one argument containing a space distinguishable from two, and stops a value from issuing a terminal escape.
env values are literal strings that ship inside the published facet. Never put a token, key, or other secret in one.

Troubleshooting

Cause: one install runs per project at a time, and another process holds the lock. The message names the process that holds it.Fix: wait for that run to finish. If it died, the lock is released when the process exits; re-run then.
Cause: the facet was built by a newer CLI, using an archive format yours does not read. This is a version mismatch, not a corrupt file.Fix: run facet self-update. The message names the minimum supporting release when it knows it.
Cause: facets.lock declares a version this CLI does not read. The common case is the withdrawn closed-alpha 1.Fix: if it declares 1, delete facets.lock and run facet install to regenerate it. If it declares something newer, run facet self-update instead, because deleting a newer lockfile discards resolutions your teammates committed.
Cause: facets.json declares an alias or omission for a text asset, and the lockfile is still at 0.2, which has no field for it. A frozen install refuses rather than ignore your intent.Fix: run one plain facet install to migrate the lockfile, then commit it.
Cause: the manifest and lockfile record different versions, sources, or materialization intent, and frozen mode never rewrites the lockfile.Fix: it depends on what drifted, and the fix: line says which case you are in.
  • Nothing stale: run facet install without the flag, or facet add, then commit the lockfile.
  • Only stale overrides: remove those materialization choices from facets.json, or re-run without the flag to drop them.
  • Both: re-run without the flag, which records the drift and drops the stale choices in one run.
Cause: a materialized file no longer matches the per-file integrity in facets.lock, usually an edit or corruption on disk.Fix: re-run facet install. It repairs a drifted file in place without touching files you added yourself.When the archive itself disagrees with the lockfile, re-running will not help. Delete facets.lock and re-run, or facet add the facet to refresh its entry.
Cause: the cache slot for that version disagrees with the integrity your lockfile records.Fix: delete that version’s directory under $FACET_DIR/cache/ and re-run, which re-downloads and re-verifies it.
Cause: the content resolved, but the registry could not confirm its published fingerprint, so writing a lockfile entry would record something unverified.Fix: restore registry access and re-run. This is not fixed by clearing the cache: the content was fine, the confirmation was missing.
Cause: an as value does not satisfy the asset-name grammar: 1 to 64 characters of lowercase letters, digits, and hyphens, with no leading, trailing, or repeated hyphen. An alias is a single segment, so team/planning is rejected too.Fix: correct the alias at the facets.json path the error names. Aliases are never auto-corrected.
Cause: the names you picked in the resolver still collide with each other or with another claimant.Fix: re-run and choose names that differ. The resolver does not reopen automatically, and nothing was written.
Cause: several connected adapters manage the same MCP configuration file. Each plans against the file as it stands, so whichever wrote second would apply a plan computed from replaced content. The run stops before approval.Fix: deselect one of the named adapters, for example with facet adapter remove. Which tool owns the file is your decision.
Cause: an adapter could not read, parse, or validate its native configuration. The usual reason is a hand edit that left the file invalid.Fix: repair the named file and re-run. The document is left byte-for-byte unchanged.One variant is not a broken file: when a declaration contains a value your tool would expand rather than use literally, the run reports an interpolation conflict naming the server and the value. Fix or omit that server in facets.json instead.
Cause: every plan is recomputed immediately before it is written. Something changed the named file in between, so what would be written is no longer what you approved.Fix: re-run. Anything the run had already written is rolled back first, and the fix: line reports what was left on disk.
Cause: a file moved between planning and writing, something that is not a plain file occupies a path, or the write failed at the filesystem level.Fix: the fix: line names the path and the reason. Resolve that one path, then re-run. Nothing is left half-applied.

See also