---
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.<ts>)"
  - "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.<ts>` 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.<timestamp>` 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
