07Operate · Troubleshooting
Read the refusal, choose the workflow.
Skill Suitcase fails closed. A refusal is a request to inspect state and choose the correct workflow, not a reason to bypass the receipt or approval boundary.
What each status asks of you
Provider fallback inventory without a catalog assignment appears with
statusCount: 0 and no status entries; a custom provider
path tied to a catalog assignment may produce ordinary status entries
while staying read-only for mutation.
Common refusal codes
| Code | Meaning |
|---|---|
| read_only_target | a target-aware materialization or mutation flow cannot adopt or write a provider-managed target; expected for OpenCode and Pi |
| source_denied_path | source policy blocked a path before a target write; also appears as diff_source_denied_path from diff |
| symlink_source_policy_exclude | symlink apply cannot enforce a non-empty sourcePolicy.exclude, even one with no current matches; use copy mode or clear the policy |
| source_untracked_files | selected Git-backed source contains untracked, non-ignored files; track or remove them before packing or applying (reported by pack and apply, not diff) |
| unsafe_target_state | apply found unmanaged or unapproved live state; choose a recovery workflow instead of forcing the install |
| plan_lock_<reason> | current facts no longer match the lock proof, for example plan_lock_current_plan_unavailable, plan_lock_target_mismatch, or plan_lock_source_mismatch; rebuild the lock from current state |
| symlink_source_escape | the requested link would leave the approved catalog root |
| symlink_target_conflict | the live target shape cannot be replaced implicitly |
| receipt_lock_failed | a mutating workflow could not acquire or use the serialized receipt transaction lock; retry after the concurrent workflow finishes |
| missing_skill_category / invalid_skill_category | a categorized Hermes assignment needs one safe category segment for every selected skill |
| ambiguous_assignment_path_layout | one assignment has both flat and categorized paths; select an explicit assignment path ID |
| hermes_external_root_unregistered | the categorized root must already exist and resolve from <home>/config.yaml skills.external_dirs |
| hermes_external_root_local_overlap / hermes_external_root_precedence_overlap | the owned root overlaps the local skills tree or an earlier configured external root |
| hermes_local_skill_shadow / hermes_external_skill_shadow / hermes_managed_skill_shadow / hermes_planned_skill_identity_conflict | a local, earlier external, owned-root, or duplicate planned SKILL.md identity would change which skill Hermes resolves |
| hermes_shadow_directory_symlink / external_category_symlink / external_destination_escape | categorized containment cannot be proven because traversal reached a symlink or escaped the owned root |
| artifact_destination_mismatch | the artifact's categorized destination differs from the current assignment; review and repack |
Recovery commands also emit state-specific refusals when the selected
skill does not meet that workflow's ownership and drift contract, for
example repair refusing unknown, missing, behind,
symlink-mode, read-only, and unselected states.
Situations that trip people up
A categorized Hermes root is refused
Create the approved root and register its exact path under
skills.external_dirs before Hermes reads it. Do not add
the path automatically. Inspect local skills and every earlier
configured external root for identity shadows or overlap, then
rerun plan, status, and diff.
Prune refuses a legacy receipt
prune requires the modern
.skill-suitcase-receipt.json and safely refuses a legacy
.skills-sync.json receipt instead of migrating it inside
a safety-sensitive workflow.
A quarantine root already exists
prune --apply refuses and preserves a pre-existing
plan quarantine root instead of reusing or cleaning it. Retain the
reported quarantine root, transaction journal, and receipt backup for
reviewed cleanup; never replace prune with manual deletion or broad
rollback.
An apply-mode refusal still says dryRun: false
A refused apply keeps dryRun: false to represent the
requested mode, sets readOnly: true, and performs no
mutation. A missing install root is refused rather than recreated.
Rollback refuses drifted state
rollback verifies that current target bytes still match
the applied receipt before reversing anything; drift is a refusal,
not something it overwrites. It also does not restore promotions:
a promotion receipt is a safe no-op whose preserved backup needs a
separate manual recovery decision.
Pack output landed somewhere unsafe
Pack refuses output beneath an absolute manifest-declared install
root, but that guard does not account for CLI target overrides and
does not expand ~. Always stage into a temporary
directory outside both the catalog and every resolved target root.
Where did my JSON go?
Structured results, including ok: false refusals, go to
stdout. Parser and usage failures, uncaught fatal diagnostics, and
non-JSON notices go to stderr. If stdout is empty, check stderr and
the exit code.
Still stuck?
Check the CLI reference for per-command preconditions, or open a support request through SUPPORT.md. Report suspected vulnerabilities privately per SECURITY.md.