Skip to content

T-AWEM-sdlc-setup-seeds-hooks-gitignore-unignore

Status: closed/done · Impact: medium · Complexity: small

Auto-generated from a /sdlc:task-work post-mortem. Review and promote to ready before picking up.

Project-local Claude Code hooks live under .claude/hooks/<script> and are intentionally shareable (just like .claude/settings.json), but the project’s .gitignore template ignores everything under .claude/ by default and only un-ignores a known short list (settings.json, skills/, commands/, agents/, hooks.json). When a project adds its first hook script the commit silently fails with “paths ignored by .gitignore” until the author hand-edits .gitignore. Closing this gap lets new projects ship hooks without tripping over the boilerplate. See T-H9Q4-document-every-skill-with-mermaid-flowchart for the originating friction.

/sdlc:setup (today plugin/skills/setup/SKILL.md, backed by plugin/skills/setup/setup_planning.py — the script is co-located with its skill per plugin/skills/CLAUDE.md’s co-locate-first rule, NOT at plugin/scripts/setup_planning.py) only creates docs/planning/<type>/ directories. It does not touch .gitignore. The script also has no --dry-run flag yet; adding one is part of this task’s scope per AC-3.

Note (2026-05-20): this project’s .gitignore already contains !.claude/hooks/ and !.claude/hooks/** — they were added inline during the originating task. The setup-skill gap remains for any fresh project adopting the plugin.

The canonical “ignore .claude/ but un-ignore the shareable bits” block reads:

# --- Claude Code project-local ---
.claude/*
!.claude/settings.json
!.claude/skills/
!.claude/commands/
!.claude/agents/
!.claude/hooks.json
!.claude/hooks/
!.claude/hooks/**

Without the trailing two lines, freshly-checked-in hook scripts under .claude/hooks/<name>.py are ignored and git add refuses without -f.

/sdlc:setup becomes responsible for ensuring .gitignore contains the canonical un-ignore block for shareable .claude/ directories, including .claude/hooks/. The skill is idempotent (no-op if the block is already present) and surgical (no other lines edited). New projects that adopt the SDLC plugin can drop a hook into .claude/hooks/ and commit it without manual .gitignore surgery.

  1. Define the canonical block as shown in the Today section (keep the existing per-bit un-ignore shape and add !.claude/hooks/ plus !.claude/hooks/**). Hard-code it as a string constant in setup_planning.py.
  2. Extend plugin/skills/setup/setup_planning.py: read .gitignore, detect the canonical block by its fence comment (# --- Claude Code project-local ---). If absent, append the full block. If present but missing the hooks lines, patch in-place. If the file doesn’t exist, create it with just the block. Leave all other lines untouched. Keep this logic in setup_planning.py directly (no sibling helper) to match the rest of the setup flow.
  3. Add a --dry-run flag to setup_planning.py. When set: report what would change in .gitignore (and any planning dirs) to stdout in a unified diff form, but do not write. Plumb the flag through any existing write paths so the flag’s semantics are “no writes anywhere,” not just gitignore.
  4. Update plugin/skills/setup/SKILL.md to describe the new side-effect and how --dry-run works.
  5. Cover with a fixture-based test under plugin/skills/setup/tests/ (this directory is greenfield; create it). Fixtures: fresh project (no .gitignore), project with the block but missing hooks lines, project with the block fully populated (no-op).
  • plugin/skills/setup/setup_planning.py — extend to ensure the canonical .gitignore block exists; add --dry-run flag.
  • plugin/skills/setup/SKILL.md — mention the new behavior and --dry-run semantics.
  • docs/skills/setup.md — refresh the flowchart to include the .gitignore step. (The skill-doc-drift warning hook from the parent task will flag this if forgotten.)
  • plugin/skills/setup/tests/ (new directory) — fixtures for the three cases above plus a --dry-run no-write assertion.
  • AC-1: /sdlc:setup on a project with no .gitignore creates one containing the canonical Claude Code block, including !.claude/hooks/ and !.claude/hooks/**.
  • AC-2: /sdlc:setup on a project whose .gitignore has the block but lacks the hooks lines inserts them in the correct position and leaves the rest of the file byte-identical.
  • AC-3: /sdlc:setup --dry-run reports what would change in .gitignore but does not write.
  • AC-4: After the skill runs, dropping a script at .claude/hooks/<name>.py and running git add succeeds without -f.
  • Migrating projects that already shipped a non-canonical .claude/ block (e.g. they un-ignored different bits). Manual reconciliation.
  • Auto-creating hook scripts.
  • none

Spawned by /sdlc:task-work post-mortem of T-H9Q4-document-every-skill-with-mermaid-flowchart on 2026-05-19. The parent task ran into the missing un-ignore line while adding .claude/hooks/warn-skill-doc-drift.py; worked around it by editing .gitignore inline.

Captured by /sdlc:task-work on 2026-05-21. PR: pending.

  • AC-1: auto — plugin/skills/setup/tests/run_evals.py case ac1-no-gitignore-creates-canonical-block runs the script against a temp project with no .gitignore, then asserts both !.claude/hooks/ and !.claude/hooks/** are in the resulting file.
  • AC-2: auto — case ac2-block-missing-hooks-gets-patched-in-place starts from a fixture whose .gitignore has the canonical fence but lacks the hooks lines, runs the script, and asserts the hooks lines were inserted while the rest of the file remained intact. The block-fully-populated-is-no-op case complements it by proving an already-canonical file is left alone.
  • AC-3: auto — cases ac3-dry-run-on-fresh-project-writes-nothing and ac3-dry-run-on-patch-case-writes-nothing assert the filesystem is unchanged after --dry-run: the fresh-project case checks no .gitignore was created, and the patch-case checks the file is byte-identical to the fixture input.
  • AC-4: auto — assert_git_add_hook_succeeds runs inside cases ac1, ac2, and no-fence-appends-block, initializing an ephemeral git repo at the fixture root, dropping .claude/hooks/demo.py, and verifying git add .claude/hooks/demo.py returns 0 and actually stages the file (without -f).
  • The existing fence comment # --- Claude Code project-local --- made block detection unambiguous — no regex acrobatics needed, just locate the fence and walk to the next blank line or top-level fence boundary.
  • The eval pattern from plugin/skills/entities-audit/tests/run_evals.py transferred cleanly: a list of (name, fixture_builder, expectations) tuples with a small custom-check hook covered every AC including the git add invariant without needing a separate harness.
  • --dry-run was already plumbed through the existing write paths; extending it to the new gitignore step was a one-line check, no refactor required.
  • The Today section asserted “the script also has no --dry-run flag yet” — but the script already had --dry-run plumbed when this task was picked up. The relevance check in /sdlc:task-work Step 2 didn’t surface that mismatch because the regex sweep only looks at file paths, not at claims-about-flags. A symbol-and-flag-claim cross-check (extract back-ticked CLI flags from Today, then grep the cited script for an add_argument(..."--<flag>")` would catch this class of staleness automatically. → T-DV8J-task-work-relevance-verifies-cli-flag-claims
  • Step 5b’s rebase of the feat branch onto main hit a conflict on the task file because ensure-ready had bumped readiness_verified_at: to a newer timestamp on the feat branch while the start-commit on main carried the older stamp. Resolution was trivial (keep both changes), but a one-shot helper that combines “stamp ensure-ready”
    • “set status: in-progress + last_reviewed” into a single edit on one branch (instead of bumping the same frontmatter on two branches separately) would eliminate the conflict entirely. → T-H98A-last-reviewed-rebase-collision

← Back to Tasks