--- phase: 07-import-cicd plan: 05 subsystem: cmd/hook tags: [cicd, git, pre-commit, hook, embed] requires: [cmd/root.go hookCmd registration] provides: [keyhunter hook install, keyhunter hook uninstall, embedded pre-commit script] affects: [cmd/stubs.go] tech-stack: added: [embed (stdlib)] patterns: [go:embed compile-time script bundling, marker-based ownership detection] key-files: created: - cmd/hook.go - cmd/hook_script.sh - cmd/hook_test.go modified: - cmd/stubs.go decisions: - "Use KEYHUNTER-HOOK v1 marker string to detect KeyHunter-owned hooks before overwrite/delete" - "Backup non-owned hooks on --force install instead of silent destruction (pre-commit.bak.)" - "Reuse same --force flag variable for install/uninstall since commands are mutually exclusive" - "Embed script via go:embed rather than string literal to preserve shell formatting and allow standalone editing" metrics: duration: ~5m completed: 2026-04-05 requirements: [CICD-01] --- # Phase 7 Plan 5: cmd/hook Install/Uninstall Summary Git pre-commit hook lifecycle via `keyhunter hook install`/`uninstall`, with the pre-commit script bundled at compile time through `go:embed`. ## What Was Built - **cmd/hook.go** — `hookCmd` parent with `install` and `uninstall` subcommands, shared `--force` flag, `.git/` validation via `hookPath()`, marker-aware overwrite/delete logic. - **cmd/hook_script.sh** — Embedded bash script carrying the `KEYHUNTER-HOOK v1` marker. Reads staged files via `git diff --cached --name-only --diff-filter=ACMR`, pipes through `xargs -r keyhunter scan --exit-code`, propagates exit code to block commits on findings. Trailing newline preserved. - **cmd/hook_test.go** — 10 tests: - `TestHookInstall_FreshRepo` — mode 0o755, marker present - `TestHookInstall_NotAGitRepo` — error when no `.git/` - `TestHookInstall_ExistingNonKeyhunterRefuses` — refuses without `--force` - `TestHookInstall_ForceBackupsExisting` — `.bak.` backup created - `TestHookInstall_ExistingKeyhunterOverwrites` — updates stale marker hook silently - `TestHookUninstall_RemovesKeyhunter` — install then remove lifecycle - `TestHookUninstall_RefusesForeign` — refuses non-marker file - `TestHookUninstall_Force` — removes foreign with `--force` - `TestHookUninstall_Missing` — no-op friendly message - `TestHookScript_ContainsRequired` — embedded script sanity check - **cmd/stubs.go** — Removed `hookCmd` stub block; kept every other stub. ## Verification ``` go build ./... # succeeds go test ./cmd/... -run Hook -v 10 tests, all PASS ``` Commit: aa8daf8 ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] Transient importCmd build error** - **Found during:** Task 1 verification (`go build`) - **Issue:** Initial `go build` failed with `undefined: importCmd` because the stubs.go linter-driven update removed `importCmd` while `cmd/import.go` wasn't visible in the workspace yet. - **Fix:** Re-scanned workspace; `cmd/import.go` was present (plan 07-04's output), no action needed beyond ensuring `stubs.go` does not redeclare `importCmd`. Final `go build` succeeds. - **Files modified:** none beyond plan scope - **Commit:** aa8daf8 (same task commit) ## Key Decisions - **Marker string as ownership signal.** `KEYHUNTER-HOOK v1` in the script header lets uninstall/overwrite distinguish our hooks from user-authored ones without an external manifest file. The `v1` suffix reserves space for future script format bumps. - **Backup-on-force rather than destroy.** `--force` install renames the existing hook to `pre-commit.bak.` instead of deleting, giving users a recovery path if they aimed the flag carelessly. - **No global `viper`/config wiring.** The hook commands are filesystem-only; introducing config loading would add surface area for zero benefit at this layer. ## Files Created/Modified - Created: cmd/hook.go (~105 lines) - Created: cmd/hook_script.sh (20 lines) - Created: cmd/hook_test.go (~180 lines) - Modified: cmd/stubs.go (removed 6-line stub block) ## Self-Check: PASSED - cmd/hook.go: FOUND - cmd/hook_script.sh: FOUND - cmd/hook_test.go: FOUND - Commit aa8daf8: FOUND - `go build ./...`: PASS - `go test ./cmd/... -run Hook`: 10/10 PASS - Stub removed from cmd/stubs.go: CONFIRMED