From nacl
Creates emergency hotfix branches from main, stashes/cherry-picks changes, validates, opens a PR with auto-merge, and restores the source branch. Use for critical production bugs requiring an urgent fix to main.
How this skill is triggered — by the user, by Claude, or both
Slash command
/nacl:tl-hotfixopusThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Inputs this skill consumes:**
Inputs this skill consumes:
Outputs this skill produces:
Downstream consumers of this output:
Contract change discipline: If this skill's output contract changes — status vocabulary, headline format, exit codes, or report-field names — every downstream consumer in the list above must be audited and updated in the same release. The 0.10.0→0.10.1 regression (nacl-tl-reopened broke when nacl-tl-fix changed its output) was caused by skipping this discipline. Do not ship contract changes without auditing consumers.
You are the hotfix specialist. When a critical bug hits production and the fix cannot wait for a feature branch to merge, you create a clean hotfix branch from main, apply the fix, open a PR with auto-merge, and restore the user to their original branch.
You are the ONLY skill authorized to target main from another branch. /nacl:tl-ship always commits to the current branch -- it never switches.
Hotfix branch from fresh main → apply fix → validate → PR with auto-merge → restore source branch
Never push directly to main without explicit --push-direct-to-main + double confirmation.
--push-direct-to-main flag AND explicit double confirmation from the user.main. The source feature branch is only restored to its pre-hotfix state.--rebase-feature flag.main (git pull first). Never from the feature branch./nacl:tl-hotfix --apply # uncommitted changes → hotfix PR to main
/nacl:tl-hotfix --cherry-pick <commit|HEAD> # existing commit → hotfix PR to main
/nacl:tl-hotfix "description" # write fix from scratch on hotfix branch
/nacl:tl-hotfix ... --push-direct-to-main # skip PR, push directly to main (double confirmation)
/nacl:tl-hotfix ... --rebase-feature # after hotfix, rebase source feature branch from main
/nacl:tl-hotfix ... --dry-run # analysis only, no git operations
/nacl:tl-hotfix ... --yes # skips non-safety prompts (task-list selection, module-detection confirmation). Does NOT bypass the pre-merge non-PASS gate at Step 6.
| Data | Source priority |
|---|---|
| Main branch | git.main_branch > modules.[name].git_base_branch > fallback "main" |
| Branch prefix | hardcoded "hotfix/" (NOT git.branch_prefix which is for features) |
| Build command | modules.[name].build_cmd > workspace package.json scripts.build. No npm run build fallback. Missing → HOTFIX HALTED — NO_INFRA (scripts.build undeclared) (P2). |
| Test command | modules.[name].test_cmd > workspace package.json scripts.test. No npm test fallback. Missing → HOTFIX HALTED — NO_INFRA (scripts.test undeclared) (P2). |
| Test filter | modules.[name].test_filter_flag (e.g. --testPathPattern, --filter) > unset. If unset, the regression-test step runs the full declared test command rather than inventing a runner flag. |
| Module path | modules.[name].path > detect from package.json |
| YouGile columns | yougile.columns.* |
| Deploy production | deploy.production.* |
Goal: Determine the scenario and validate preconditions.
config.yaml for git settings, deploy config, module config.current_branch=$(git rev-parse --abbrev-ref HEAD)
--apply flag: Scenario 1 (uncommitted changes). Verify git status --porcelain shows changes.--cherry-pick <hash> flag: Scenario 2 (existing commit). Verify commit exists: git cat-file -t <hash>.{main_branch} is reachable:
git fetch origin {main_branch}
Present triage summary to user:
Scenario: {1: uncommitted changes | 2: cherry-pick | 3: from scratch}
Source branch: {branch_name}
Main branch: {main_branch} (fetched: OK)
Changes: {file count | commit hash | "will be authored"}
Goal: Preserve the feature branch state and create the hotfix branch.
Scenario 1 (uncommitted changes):
git stash push -m "hotfix-stash-$(date +%s)"
Record the stash reference.
Scenario 2 (existing commit): No stash needed. Record the commit hash to cherry-pick.
Scenario 3 (from scratch): No stash needed. The fix will be written on the hotfix branch.
All scenarios — create hotfix branch from fresh {main_branch}: the slug comes from the
single-authority formatter (same lowercase/hyphens/≤50 rule as /nacl:tl-ship; pinned by
${CLAUDE_PLUGIN_ROOT}/nacl-core/scripts/branch.test.sh). (Hotfix branches from {main_branch} by design — the
base-branch GUARD does not apply here.)
git checkout {main_branch}
git pull origin {main_branch}
slug=$(bash ${CLAUDE_PLUGIN_ROOT}/nacl-core/scripts/branch.sh slug "$description")
git checkout -b "hotfix/${slug}"
Example: hotfix/cast-lectureid-uuid-generation-query
Goal: Get the fix code onto the hotfix branch.
Scenario 1 (from stash):
Before applying the stash, invoke /nacl:tl-regression-test against the current hotfix branch
(which still has main's code — the stash has NOT been applied yet). Provide:
git stash show -p stash@{0} file list)The regression test MUST be RED on main's code before the stash is applied. If the test is GREEN
(does not capture the bug), halt and ask the user to sharpen the bug description before retrying.
Record the test path as regression_test_path.
git stash apply stash@{0}
If conflicts:
Scenario 2 (cherry-pick):
Before cherry-picking, invoke /nacl:tl-regression-test against the current hotfix branch
(which still has main's code — the cherry-pick has NOT been applied yet). Provide:
git diff <commit>~1 <commit> --name-only)The regression test MUST be RED on main's code before the cherry-pick is applied. If the test is
GREEN (does not capture the bug), halt and ask the user to sharpen the bug description.
Record the test path as regression_test_path.
git cherry-pick <commit-hash> --no-commit
Using --no-commit to allow inspection before committing.
If cherry-pick fails:
git cherry-pick --abort
# Fallback: generate and apply patch
git diff <commit>~1 <commit> | git apply --3way
If both fail: STOP and report the dependency issue (the fix likely depends on code that only exists in the feature branch).
Scenario 3 (from scratch):
Delegate to /nacl:tl-fix with the user's description. Wait for fix to complete.
Capture /nacl:tl-fix's Status: field explicitly from its Step 8 report. This is
mandatory — the production main bar is higher than a feature branch.
Branch on the status immediately:
--yes was supplied.)HOTFIX HALTED — REGRESSION
Fix introduced new test failures not present in baseline, or the regression test
for this bug is still failing after the fix.
Do NOT ship. Return to /nacl:tl-fix Step 6f to correct the fix.
Record the new failures in YouGile task chat as advisory (user decides whether to
open a new task). Do NOT proceed.Goal: Confirm that a regression test was actually written and ran RED→GREEN. This step applies to ALL scenarios.
Scenario 3: Parse the /nacl:tl-fix Step 8 report for both of the following fields:
Regression test: [path of new test (Path A) | "covered by existing test: [path]" (Path B) | ...]
RED→GREEN: [✓ confirmed at 6e and 6g (Path A) | ✓ existing test transitioned (Path B) | ...]
Regression test: field is missing, empty, or reads "none — UNVERIFIED": emit:
HOTFIX HALTED — UNVERIFIED (regression-test seam not honored)
/nacl:tl-fix did not produce a regression test (Path B "UNVERIFIED" or field absent).
Cannot confirm the fix is verifiable. Return to /nacl:tl-fix Step 6 with sharper inputs.
HALT. Do NOT proceed.RED→GREEN: field is missing or reads ✗: emit:
HOTFIX HALTED — UNVERIFIED (regression-test seam not honored)
/nacl:tl-fix reported a test path but RED→GREEN evidence is absent or negative.
The test did not prove the fix works. Return to /nacl:tl-fix Step 6e/6g.
HALT. Do NOT proceed.regression_test_path (exact absolute path from the field) and
red_green_evidence (the summary from the RED→GREEN line). Continue to Step 4.Scenarios 1 and 2: regression_test_path was recorded in Step 3 above (the test written
before the stash/cherry-pick was applied). Confirm it is non-empty. If it is empty (the
regression test invocation was skipped or failed to produce a path), halt:
HOTFIX HALTED — UNVERIFIED (regression-test seam not honored)
No regression test path was recorded for Scenario {1|2}. Cannot proceed without test evidence.
Goal: Verify the fix works on the main branch codebase, including a named regression-test run, and classify any remaining failures against a captured main baseline (P3).
Before running the postfix suite, capture a {main_branch} baseline using
git worktree add (no stash juggling on the active hotfix branch):
baseline_dir=$(mktemp -d -t hotfix-baseline.XXXXXX)
git worktree add "$baseline_dir" "$(git rev-parse {main_branch})"
( cd "$baseline_dir/[module_path]" && [test_cmd] ) > "$baseline_dir/baseline.log" 2>&1 || true
Parse the failing-test set from baseline.log into baseline_failures (a set of
test identifiers). The || true is required — the baseline is permitted to be
non-clean; we only care about which tests fail there.
The baseline worktree MUST be removed at the end of the skill regardless of outcome (success, halt, or error). The cleanup step (Step 7 / Cleanup on Failure) runs:
git worktree remove -f "$baseline_dir" 2>/dev/null || true
rm -rf "$baseline_dir"
If git worktree add fails (worktree already exists, disk full, etc.):
emit HOTFIX HALTED — NO_INFRA (baseline worktree unavailable) and abort
before applying the fix on the hotfix branch.
Run the regression test by file path (from regression_test_path recorded in
Step 3.5). Use the declared test command plus the configured test_filter_flag
(see Configuration Resolution). If test_filter_flag is unset, run the full
declared test command — do NOT invent a generic --test-name-pattern flag (P2):
if [ -n "$test_filter_flag" ]; then
cd [module_path] && [test_cmd] $test_filter_flag "[regression_test_path]"
else
# No declared filter syntax for this runner; run the full suite.
cd [module_path] && [test_cmd]
fi
The regression test MUST be GREEN. If it is RED (still failing after the fix was applied), halt:
HOTFIX INCOMPLETE — REGRESSION
Regression test {regression_test_path} is still failing after the fix.
The fix does not address the bug. Return to Step 3 / /nacl:tl-fix Step 6f.
HALT. Do NOT proceed.
Run the full test suite on the hotfix branch:
cd [module_path] && [test_cmd] > postfix.log 2>&1
postfix_exit=$?
Parse postfix_failures from postfix.log. Compute set differences:
new_failures = postfix_failures − baseline_failures
transitioned = baseline_failures − postfix_failures
pre_existing = postfix_failures ∩ baseline_failures
Run build:
cd [module_path] && [build_cmd]
Classify the outcome (P3 — no claim about pre-existing failures without the captured baseline):
new_failures.size > 0 → REGRESSION.
Halt with HOTFIX INCOMPLETE — REGRESSION (new failures: <list>). Do NOT
proceed. Record the new failures in YouGile task chat.
new_failures.size == 0 AND postfix_failures.size > 0 → BLOCKED
(pre-existing failures confirmed by baseline). Record pre_existing set
explicitly in the report — these are the same tests that fail on main,
not regressions from this hotfix. Continue to Step 5 with status BLOCKED;
Step 6 gate fires.
postfix_failures.size == 0 AND build OK → PASS (continue normally).
[test_cmd] returned NO_INFRA / RUNNER_BROKEN (e.g. zero tests collected,
runner crashed) → emit HOTFIX BLOCKED — {NO_INFRA | RUNNER_BROKEN}. The
hotfix code itself may be correct, but cannot be verified. Step 6 gate fires.
Do NOT claim a failure is "pre-existing", "unrelated", or "may not be
related" without baseline_failures set membership confirming it.
If the failure is a dependency issue (code from feature branch not on {main_branch}): STOP with advisory:
This fix depends on code from {source_branch} that does not exist on {main_branch}.
Options:
(a) Include the dependency in the hotfix
(b) Merge the full feature branch first
(c) Write a standalone fix for {main_branch}
Show impact summary:
git diff --stat {main_branch}..HEAD
Goal: Stamp Task.verification_evidence for every affected Task node so
that release/reporting tooling sees the proof of RED→GREEN (taxonomy:
${CLAUDE_PLUGIN_ROOT}/nacl-core/SKILL.md § Task.verification_evidence).
Collect the affected task IDs from /nacl:tl-fix's Step 8 triage report
(the Affected UCs list, plus any BUG-### / TECH-### the fix declared).
If the fix report contained IMPACT_UNVERIFIED, do NOT guess — log a warning
and skip the graph write. The PR still proceeds, but the release skill will
surface this UC as a verification gap until the operator reconciles.
Compose $evidence:
regression_test_path from fix:
$evidence = 'test-GREEN:' + regression_test_path (normalise to repo-relative).$evidence = 'test-UNVERIFIED' (write only after the Step 6 override is
confirmed; do NOT write at Step 4.3 in this case — see Step 6).Cypher (PASS path only at this step):
UNWIND $affectedTaskIds AS taskId
MATCH (t:Task {id: taskId})
SET t.verification_evidence = $evidence,
t.updated = datetime()
Use mcp__neo4j__write-cypher. If Neo4j is unavailable, log a warning
and continue — the PR is the source of truth for the test path until
the operator runs /nacl:tl-reconcile. Do NOT block the hotfix on a
graph-write failure.
If the user passed --push-direct-to-main (bypasses PR), still write evidence:
the test path is no less real because the operator skipped the PR gate.
Goal: Create a clean commit on the hotfix branch.
git add [relevant files]
hotfix: prefix (not fix:) to clearly distinguish in git log:
git commit -m "$(cat <<'EOF'
hotfix: {description}
Applied as hotfix from {source_branch}.
Scenario: {1: stash apply | 2: cherry-pick <hash> | 3: authored on hotfix branch}
Co-Authored-By: Claude <[email protected]>
EOF
)"
Goal: Get the fix to main via PR with auto-merge.
Pre-merge status gate (mandatory — runs before the user gate even when --yes was supplied):
Check the fix status captured in Step 3 (Scenario 3) or Step 4:
If status is PASS: proceed to the standard user gate below. The PR
headline is HOTFIX COMPLETE; auto-merge is enabled.
If status is anything other than PASS: issue a fresh unconditional prompt regardless
of whether --yes was supplied at invocation time. The --yes flag does NOT satisfy this
gate. Present:
⚠ Fix status: {STATUS}. Reason: {reason from Step 3 / Step 4}.
Shipping a non-PASS fix to main is high-risk and cannot be auto-confirmed.
Confirm to proceed? [yes/no]
Default: no
If the answer is not explicitly "yes", STOP. Do not create the PR.
This gate is unconditional: --yes scope is limited to non-safety prompts
(task-list selection, module-detection confirmation). It never bypasses this gate.
If the operator confirms "yes" on a non-PASS status (UNVERIFIED, BLOCKED,
NO_INFRA, RUNNER_BROKEN), the resulting PR is annotated as
HOTFIX APPLIED — UNVERIFIED with the captured status and reason in the
PR description (P4). Auto-merge is not enabled — gh pr merge --auto
is skipped; the operator must run a separate manual merge command after
reviewing CI and the captured baseline diff. The advisory Status: line is
written to the report and to YouGile.
REGRESSION never reaches Step 6 — it halts at Step 4.2.
Present plan to user (unless --yes):
═══════════════════════════════════════════════
HOTFIX READY TO SHIP
═══════════════════════════════════════════════
Branch: hotfix/{slug}
Target: {main_branch}
Commit: {hash}
Files changed: {N}
Fix status: {PASS | BLOCKED | UNVERIFIED | NO_INFRA | RUNNER_BROKEN | REGRESSION}
The hotfix PR will be created with auto-merge.
CI must pass before merge. After merge, production
deploy triggers automatically via {deploy.production.trigger}.
Proceed? [yes/no]
═══════════════════════════════════════════════
Standard path (PR + auto-merge):
git push -u origin hotfix/{slug}
gh pr create \
--title "hotfix: {description}" \
--body "$(cat <<'EOF'
## Hotfix
**Priority:** Critical -- production fix
**Headline:** {HOTFIX COMPLETE | HOTFIX APPLIED — UNVERIFIED}
**Status:** {PASS | BLOCKED | UNVERIFIED | NO_INFRA | RUNNER_BROKEN}
**Source:** {source_branch} (Scenario {N})
{if non-PASS: "⚠ Auto-merge is DISABLED — operator override on non-PASS fix. Reason: {reason}. Review CI, baseline diff, and the captured `pre_existing` set before merging manually."}
### Regression Test Evidence
**Regression test:** {regression_test_path}
**RED→GREEN evidence:** {red_green_evidence — e.g. "✓ confirmed at Step 6e (RED) and 6g (GREEN) by nacl-tl-fix" or "✓ confirmed RED on main before stash/cherry-pick, GREEN after"}
### Baseline (main) vs Postfix (hotfix)
- Baseline failures (on main, captured via `git worktree`): {N} — {list}
- Postfix failures (on hotfix branch): {N} — {list}
- New failures (postfix − baseline): {N} — {list} ← MUST be 0 to ship
- Pre-existing (postfix ∩ baseline): {N} — {list}
### Changes
{git diff --stat summary}
### Test Plan
- [x] Regression test GREEN on hotfix branch ({regression_test_path})
- [x] Postfix suite has zero new failures vs main baseline
- [x] Build succeeds
- [{x or " "}] CI pipeline (auto-merge {enabled | DISABLED — non-PASS override})
Generated with Claude Code via /nacl:tl-hotfix
EOF
)" \
--base {main_branch} \
--label "hotfix"
# Auto-merge is enabled ONLY when fix status is PASS.
# Non-PASS overrides leave auto-merge OFF — operator runs the merge manually
# after reviewing CI and the baseline diff.
if [ "$fix_status" = "PASS" ]; then
gh pr merge --auto --squash
else
echo "Auto-merge skipped — fix status is $fix_status. Review the PR manually." >&2
fi
Direct-push path (with --push-direct-to-main flag; renamed
from the legacy flag name in W4-blocking-release to satisfy the
literal-token grep acceptance check — behavior is unchanged):
Requires double confirmation:
⚠ DIRECT PUSH TO MAIN ⚠
You are about to push directly to {main_branch}.
This triggers production deploy immediately.
Are you absolutely sure? [yes/no]
If confirmed:
git push origin hotfix/{slug}:{main_branch}
Goal: Return the user to their original branch in a clean state.
git checkout {source_branch}
Scenario 1 (was stashed):
git stash pop
The changes are now back as uncommitted on the feature branch (same state as before hotfix).
If stash pop conflicts: leave stash in place, warn user to run git stash pop manually.
Scenario 2 (cherry-picked): No stash to restore. The commit still exists on the feature branch. When the user later rebases from main, git will detect the duplicate and skip it cleanly (cherry-pick is dedup-safe).
Scenario 3 (authored from scratch): No stash. The fix exists only on the hotfix branch / main. The feature branch is unmodified.
If --rebase-feature flag:
git rebase {main_branch}
If rebase conflicts: report and abort (git rebase --abort). The user can resolve manually.
Always — remove the baseline worktree:
git worktree remove -f "$baseline_dir" 2>/dev/null || true
rm -rf "$baseline_dir"
This runs on every exit path (success, halt, error) — see also Cleanup on Failure.
Goal: Inform the user about downstream effects.
Feature branch rebase advice (if not already rebased):
Recommendation: rebase your feature branch to pick up the hotfix:
git checkout {source_branch}
git rebase {main_branch}
Other feature branches scan:
# Files changed by hotfix
hotfix_files=$(git diff --name-only {main_branch}~1..{main_branch})
# Active feature branches
git branch -r --list 'origin/feature/*'
For each remote feature branch, check if it touches the same files:
git diff --name-only {main_branch}...origin/feature/X
If any intersection, report:
Feature branches that may need rebase after hotfix merges:
- feature/FR-002-user-settings (touches: src/auth/service.ts)
- feature/FR-005-dashboard (touches: src/shared/utils.ts)
YouGile update (if configured): Post hotfix notification to relevant task chat:
send_task_message(taskId, "
🔥 HOTFIX shipped to {main_branch}
PR: #{pr_number}
Commit: {hash}
Description: {description}
Production deploy will trigger via {deploy.production.trigger}.
Monitor: /nacl:tl-deploy --production
")
If YouGile not configured -> skip, report locally.
Goal: Complete summary.
Present in user's language:
═══════════════════════════════════════════════
{HOTFIX COMPLETE | HOTFIX APPLIED — UNVERIFIED}
═══════════════════════════════════════════════
Hotfix: {description}
Scenario: {1: stash apply | 2: cherry-pick | 3: authored}
Status: {PASS | BLOCKED | UNVERIFIED | NO_INFRA | RUNNER_BROKEN}
Git:
Source branch: {source_branch}
Hotfix branch: hotfix/{slug}
PR: #{number} (auto-merge: {enabled | DISABLED — non-PASS override})
Commit: {hash}
Target: {main_branch}
Validation:
Baseline (main) failures: {N}
Postfix (hotfix) failures: {N}
New failures (postfix − baseline): {N} ← MUST be 0 to ship
Pre-existing (postfix ∩ baseline): {N}
Build: {OK | FAIL}
Source branch restored: {source_branch}
Working directory: {clean | uncommitted changes restored}
Feature branches to rebase:
{list or "none detected"}
Production:
Deploy triggers via: {deploy.production.trigger}
Monitor: /nacl:tl-deploy --production
Next steps:
/nacl:tl-deploy --production -- monitor production deploy
/nacl:tl-release --patch -- create patch release after deploy
═══════════════════════════════════════════════
Valid scenario. Skip stash. Create hotfix branch from current HEAD.
git checkout -b hotfix/{slug}
Report conflicting files after git stash apply. User must resolve or simplify the fix.
STOP and present options -- do NOT attempt auto-resolution.
Build/test failure on hotfix branch reveals this. STOP with advisory:
This fix depends on code from {source_branch} that does not exist on main.
Options:
(a) Include the dependency in the hotfix
(b) Merge the full feature branch first
(c) Write a standalone fix for main
Auto-merge will not proceed. Advise user to check CI logs:
gh run list --branch hotfix/{slug}
Each gets its own hotfix/ branch and separate PR. They serialize through main via separate PRs. No special handling needed.
STOP at Step 6 with:
GitHub CLI (gh) is required for PR creation.
Install: brew install gh
Authenticate: gh auth login
Leave stash in place. Warn user:
Stash pop had conflicts. Your stash is preserved.
Run manually: git stash pop
Then resolve conflicts.
Warn that auto-merge won't work (no status checks). Fall back to manual merge suggestion or --push-direct-to-main.
Use all defaults (main_branch: "main", etc.).
Skip all YouGile operations. Report locally only.
If ANY step fails and the workflow cannot continue:
git checkout {source_branch}git stash pop (restore working directory)git branch -D hotfix/{slug} (delete local)git push origin --delete hotfix/{slug}git worktree remove -f "$baseline_dir" 2>/dev/null || true
rm -rf "$baseline_dir"
This runs on every exit path, success or failure.config.yaml → git.main_branch, deploy.production.*nacl-tl-ship/SKILL.md — regular shipping (always current branch)nacl-tl-fix/SKILL.md — spec-first bug fixing (used in Scenario 3); Status vocabulary: PASS / BLOCKED / UNVERIFIED / NO_INFRA / RUNNER_BROKEN / REGRESSIONnacl-tl-deploy/SKILL.md — production deployment monitoring${CLAUDE_PLUGIN_ROOT}/nacl-tl-core/references/commit-conventions.md — commit message formatnpx claudepluginhub itsalt/nacl --plugin naclApplies a minimal, targeted fix for emergency bugs with enforced review and testing, skipping brainstorm and planning phases.
Provides a structured workflow for critical production hotfixes: branch from release tag, minimal fix, fast-track review, quality gates, merge and release. Includes rollback steps.
Starts git-flow hotfix branch from production tag, bumps version in files like package.json/Cargo.toml, commits, and pushes to origin. Use for urgent prod fixes.