fix(ship): verify pre-commit execution (sc-1537) - #380
Conversation
Fixes Story #1537. Root cause: ship projected the Husky hook when core.hooksPath was unset, but git commit still used Git's empty default hooks directory. The unconditional success banner therefore claimed gates ran without evidence. This change shares hook resolution between preparation and commit, fails closed when no executable pre-commit hook exists, and requires an attempt-specific execution marker before publishing. Private direct-exec wrappers preserve Husky's real path semantics for sibling hooks such as commit-msg. Setup and proof failures now emit accurate terminal telemetry. Validation: - 125 affected ship tests passed - full suite: 3,740 passed, 5 skipped - typecheck and lint passed - GitNexus scope low; duplication, correctness, and commit-guard reviews passed
📝 WalkthroughWalkthrough
ChangesHook capture and validation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant commit_with_gate_capture
participant gate_worktree_pre_commit
participant PrivateHookWrapper
participant Git
participant GateTelemetry
commit_with_gate_capture->>gate_worktree_pre_commit: Resolve executable pre-commit hook
gate_worktree_pre_commit-->>commit_with_gate_capture: Return hook path
commit_with_gate_capture->>PrivateHookWrapper: Create private wrapper
Git->>PrivateHookWrapper: Execute pre-commit hook
PrivateHookWrapper-->>GateTelemetry: Record execution marker and gate output
commit_with_gate_capture->>GateTelemetry: Record commit outcome
commit_with_gate_capture->>Git: Rewind commit if proof is missing
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
cli/lib/ship/commit-with-gate-capture.sh (2)
118-125: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider creating the wrapper directory outside the ship worktree.
mktemp -d "$wt/.devkit-ship-hooks.XXXXXX"places an untracked directory inside the worktree that the gate chain inspects. Any gate that asserts a clean working tree, or that stages withgit add -A, sees this directory. The wrappers do not need to live in the worktree: every wrapper execs$DEVKIT_SHIP_REAL_HOOKS_DIR/<hook>by absolute path, andcore.hooksPathaccepts an absolute value.If you move the directory to
${TMPDIR:-/tmp}, setcore.hooksPathto the absolute$ship_hook_dirand drop theship_hook_relderivation.♻️ Proposed change to isolate the wrapper directory
if [ "$hook_setup_failed" -eq 0 ]; then - ship_hook_dir=$(mktemp -d "$wt/.devkit-ship-hooks.XXXXXX") || { + ship_hook_dir=$(mktemp -d "${TMPDIR:-/tmp}/devkit-ship-hooks.XXXXXX") || { hook_setup_error="ship: could not create the private hook wrapper — gates must not fail open" hook_setup_failed=1 rc=1 } fi if [ "$hook_setup_failed" -eq 0 ]; then - ship_hook_rel=${ship_hook_dir#"$wt/"} + ship_hook_rel=$ship_hook_dir🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cli/lib/ship/commit-with-gate-capture.sh` around lines 118 - 125, Update the hook-wrapper setup in commit-with-gate-capture.sh to create ship_hook_dir under ${TMPDIR:-/tmp} rather than inside wt, then configure core.hooksPath with the absolute ship_hook_dir. Remove the ship_hook_rel derivation and adjust dependent setup or cleanup logic to use the absolute directory directly, preserving the existing failure handling.
129-143: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winThe subshell masks a
catwrite failure.The subshell exit status is the status of its last command,
chmod 700. Ifcatfails part-way, for example on a full disk,chmodstill succeeds on the truncated or empty file. The setup then reports success and installs a wrapper that does not exec the real hook.The proof check at Line 200 still rewinds the commit, so the ship does not fail open. The operator instead sees "NO pre-commit execution proof" rather than the real cause. Chain the two commands so the write failure surfaces as a hook-setup error.
♻️ Proposed fix to propagate the write failure
if ! (umask 077; cat > "$ship_hook_dir/$source_name" <<'SHIP_HOOK_WRAPPER' #!/bin/sh hook_name=${0##*/} if [ "$hook_name" = pre-commit ]; then printf '%s\n' "$DEVKIT_SHIP_HOOK_MARKER" >&2 fi exec "$DEVKIT_SHIP_REAL_HOOKS_DIR/$hook_name" "$@" SHIP_HOOK_WRAPPER - chmod 700 "$ship_hook_dir/$source_name"); then + && chmod 700 "$ship_hook_dir/$source_name"); then🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cli/lib/ship/commit-with-gate-capture.sh` around lines 129 - 143, Update the wrapper creation block around the SHIP_HOOK_WRAPPER heredoc so the cat write and chmod commands are chained, causing a cat failure to propagate instead of being masked by chmod. Preserve the existing hook_setup_error, hook_setup_failed, and rollback behavior when either setup command fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@cli/lib/ship/commit-with-gate-capture.sh`:
- Around line 118-125: Update the hook-wrapper setup in
commit-with-gate-capture.sh to create ship_hook_dir under ${TMPDIR:-/tmp} rather
than inside wt, then configure core.hooksPath with the absolute ship_hook_dir.
Remove the ship_hook_rel derivation and adjust dependent setup or cleanup logic
to use the absolute directory directly, preserving the existing failure
handling.
- Around line 129-143: Update the wrapper creation block around the
SHIP_HOOK_WRAPPER heredoc so the cat write and chmod commands are chained,
causing a cat failure to propagate instead of being masked by chmod. Preserve
the existing hook_setup_error, hook_setup_failed, and rollback behavior when
either setup command fails.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3e5f52b9-4cb0-433a-82c4-a19617753410
⛔ Files ignored due to path filters (2)
dist/cli/lib/ship/commit-with-gate-capture.shis excluded by!**/dist/**dist/cli/lib/ship/prepare-gate-worktree.shis excluded by!**/dist/**
📒 Files selected for processing (4)
cli/__tests__/commit-with-gate-capture.test.mtscli/__tests__/review-private-dependencies.test.mtscli/lib/ship/commit-with-gate-capture.shcli/lib/ship/prepare-gate-worktree.sh
Fixes Story #1537.
Root cause: ship projected the Husky hook when core.hooksPath was unset, but git commit still used Git's empty default hooks directory. The unconditional success banner therefore claimed gates ran without evidence.
This change shares hook resolution between preparation and commit, fails closed when no executable pre-commit hook exists, and requires an attempt-specific execution marker before publishing. Private direct-exec wrappers preserve Husky's real path semantics for sibling hooks such as commit-msg. Setup and proof failures now emit accurate terminal telemetry.
Validation:
Summary by CodeRabbit