Skill Suitcasedocs

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

currentnothing to do
missingstage with pack, review, then apply with a lock or artifact
versionthe receipt version differs from current SKILL.md frontmatter; review the change, then stage and apply the catalog update
behindthe target is unchanged but the catalog moved; stage and apply the update
dirtystop and inspect; repair --dry-run to discard the drift, import-target --dry-run to keep it, either apply only after explicit approval
blockedread the blocked reason; fix the manifest, compatibility, or policy problem in the catalog
unknownstatus could not be proven; only the receiptless mismatched-directory case routes to reconcile, everything else needs manual inspection

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

CodeMeaning
read_only_targeta target-aware materialization or mutation flow cannot adopt or write a provider-managed target; expected for OpenCode and Pi
source_denied_pathsource policy blocked a path before a target write; also appears as diff_source_denied_path from diff
symlink_source_policy_excludesymlink apply cannot enforce a non-empty sourcePolicy.exclude, even one with no current matches; use copy mode or clear the policy
source_untracked_filesselected 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_stateapply 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_escapethe requested link would leave the approved catalog root
symlink_target_conflictthe live target shape cannot be replaced implicitly
receipt_lock_faileda mutating workflow could not acquire or use the serialized receipt transaction lock; retry after the concurrent workflow finishes
missing_skill_category / invalid_skill_categorya categorized Hermes assignment needs one safe category segment for every selected skill
ambiguous_assignment_path_layoutone assignment has both flat and categorized paths; select an explicit assignment path ID
hermes_external_root_unregisteredthe categorized root must already exist and resolve from <home>/config.yaml skills.external_dirs
hermes_external_root_local_overlap / hermes_external_root_precedence_overlapthe 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_conflicta 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_escapecategorized containment cannot be proven because traversal reached a symlink or escaped the owned root
artifact_destination_mismatchthe 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.