name: fwd:setup
description: Setup wizard for HeadingFWD's optional Claude Code conventions. Asks the user in a single multiselect dialog which features to install (currently smartlint Stop-hook, a lessons memory file, gitignore entries for fwd runtime artefacts, Claude Code's clear-context prompt on plan accept, and disabling Claude Code's default commit/PR attribution), then runs the matching installers in batch — copying bundled payload files into .claude/hooks/ or .claude/lessons/, merging JSON snippets into ~/.claude/settings.json or .claude/settings.local.json, injecting an instructions section into CLAUDE.md (lessons), or appending a marker-bracketed block to .gitignore. Idempotent and modular — each feature lives in scripts//. Use only when the user invokes /fwd:setup explicitly.
disable-model-invocation: true
fwd:setup
Walk the user through installing HeadingFWD's optional conventions in a single dialog. The wizard asks once — scope + which features — then runs every selected installer in batch and shows a single summary. Each feature is independent: its installer lives in scripts/<feature>/, accepts --scope user|project, and is idempotent. Re-running the skill is safe.
Process
1. Pre-flight — pick the scope default
Run rtk git rev-parse --is-inside-work-tree (exit 0 = inside a git work tree). The result decides which scope option to list first in the dialog:
- Inside a repo →
Project-localfirst (more likely the user wants per-repo config). - Outside a repo →
User-globalfirst.
Do not install anything yet — this only changes option ordering in step 2.
2. Ask once — scope and features
Call AskUserQuestion with two questions in one call:
Question 1 — scope (single-select, no preview):
- Project-local — payload lands under
<cwd>/.claude/<feature-dir>/; settings/instructions land in the project's settings file or CLAUDE.md. - User-global — payload lands under
~/.claude/<feature-dir>/; settings/instructions land under~/.claude/. Pick this when the user wants the convention to apply everywhere they run Claude Code.
Question 2 — features (multiSelect: true, no preview, 5 options). Each option's description is one short sentence — the WHY. Match the conversation language (Dutch if the session is in Dutch).
- Smartlint Stop-hook — automatic lint after every Claude response, so code quality stays consistent without you remembering to run linters.
- Lessons memory file — Claude remembers corrections, conventions and patterns across sessions instead of starting each conversation blank.
- Gitignore entries for fwd runtime artefacts — without this,
/loop /fwd:issue-fixwrites.claude/issue-loop/state.jsonand then skips every overnight tick onmain-checkout-dirty. - Clear-context prompt on plan accept — surfaces a "Clear context?" prompt right after
ExitPlanModeso the implementation session doesn't carry the deliberation that produced the plan. - Disable commit & PR attribution — keeps the default
🤖 Generated with [Claude Code]byline andCo-Authored-By: Claude …trailer out of commits and pull requests.
If the user selects zero features, skip step 3 and go straight to the summary in step 4.
3. Apply selected installers in batch
For each selected feature, in this fixed order, run its installer with --scope <chosen> and capture the exit code and stderr. Do not abort on a non-zero exit — keep going so one feature's collision doesn't block the rest.
1. smartlint → bash "${CLAUDE_SKILL_DIR}/scripts/smartlint/install.sh" --scope <scope>
2. lessons → bash "${CLAUDE_SKILL_DIR}/scripts/lessons/install.sh" --scope <scope>
3. gitignore → bash "${CLAUDE_SKILL_DIR}/scripts/gitignore/install.sh" --scope <scope>
4. clear-context-on-plan → bash "${CLAUDE_SKILL_DIR}/scripts/clear-context-on-plan/install.sh" --scope <scope>
5. no-attribution → bash "${CLAUDE_SKILL_DIR}/scripts/no-attribution/install.sh" --scope <scope>
Per-feature exit code semantics (uniform across all installers):
- 0 — installed (fresh) or already present / refreshed in place. Idempotent.
- 2 — collision: the installer found a conflicting existing value and refused to overwrite. The installer printed the conflicting entry + remediation hint to stderr. Capture that stderr verbatim for the summary; do not retry.
- other non-zero — argument or I/O error. Capture stderr for the summary.
Feature-specific collision causes (for context when relaying stderr):
- smartlint — existing Stop-hook with a different command.
- lessons — corrupt sentinel markers in CLAUDE.md (start without end, or out of order).
- gitignore — corrupt sentinel markers in
.gitignore/~/.config/git/ignore. - clear-context-on-plan —
showClearContextOnPlanAcceptexplicitly set tofalse. - no-attribution —
attribution.commitorattribution.prholds a non-empty custom string.
4. Summary
Print one consolidated summary covering the whole run:
- Scope —
Project-localorUser-global, and the resolved paths (the settings file and CLAUDE.md that were touched, or would have been). - Per feature:
- ✓ installed / refreshed (exit 0)
- ⚠ collision (exit 2) — relay the installer's stderr verbatim with the suggested fix and the re-run instruction (
re-run /fwd:setup) - ✗ I/O error (other non-zero) — relay stderr
- skipped (not selected)
- Idempotency note — re-running
/fwd:setupis safe.
If the user selected zero features, the summary is just: scope shown (informational), no features installed, no files touched.
Invariants
- Skill is invoked explicitly only —
disable-model-invocation: truekeeps the wizard out of automatic-trigger flows. - The wizard asks at most one
AskUserQuestioncall per run — scope + features in a single dialog. No per-feature follow-up prompts. - Each feature installer lives at
scripts/<feature>/install.sh, accepts--scope user|project, and is idempotent. Re-runs detect existing installs and either refresh in place or exit 2 on a corrupt/conflicting state. - JSON merging (smartlint, clear-context-on-plan, no-attribution): the shared
scripts/lib/merge-json.shdeep-merges objects, concatenates arrays, and lets new scalars win on conflict. Each installer dedupe-checks its own entries before calling the merger so re-runs do not double the array.jqis required; when missing the merger backs up the target to<file>.bakand replaces it with the new snippet. - Markdown injection (lessons): the lessons installer brackets its section with sentinel HTML comments (
<!-- fwd:lessons:start -->/<!-- fwd:lessons:end -->) and uses inlinehead/tailto replace the region between them on refresh. No shared lib yet — extract toscripts/lib/merge-markdown.shif a second feature also needs it. - Gitignore injection: marker-bracketed block (
# fwd:setup:gitignore:start/# fwd:setup:gitignore:end) in.gitignore(project) or~/.config/git/ignore(user). - Hook commands use literal
$HOME(user) or$CLAUDE_PROJECT_DIR(project) — Claude Code's shell expands them at hook runtime. Lessons-file paths in the injected CLAUDE.md section follow the same convention ($HOME/...or repo-relative). - One feature's collision (exit 2) never aborts the batch — the remaining installers still run.
Adding a new feature later
- Create
scripts/<feature>/install.sh(accept--scope user|project, idempotent, exit 2 on collision). - Drop static files in
scripts/<feature>/payload/. Use placeholders (__HOOK_DIR__,__LESSONS_PATH__, etc.) for path substitution at install time. - Pick a merge style appropriate to what you're injecting:
- JSON settings → call
../lib/merge-json.sh - Markdown / CLAUDE.md section → use sentinel markers + inline
head/tail(seescripts/lessons/install.sh) - Plain file copy → just
cpinto<scope>/.claude/<feature-dir>/
- JSON settings → call
- Wire the feature into the single multiselect dialog in section 2 — add one option with a one-sentence WHY in its
description. No preview, no per-feature follow-up question. - Wire the feature into the apply-loop in section 3 — add it to the fixed order with the path to its
install.sh. - Add the feature-specific collision cause to the bullet list in section 3.