feat(trace): skip Claude Code and Cursor hooks when the Chainloop CLI is missing

BE
3h ago
+370-21

Closes #3687

The Claude Code and Cursor hooks run chainloop trace hook … directly, so in a repository with the hooks committed but without the CLI on PATH, every hook fails with exit 127 before Chainloop's best-effort handling can run. This wraps them in the guard from the issue, the same one #3684 added for Codex:

sh -c 'if ! command -v chainloop >/dev/null 2>&1; then exit 0; fi; exec chainloop trace hook claude post-tool-use'
  • A new trace.GuardHookCommand(command) builds that string. Both providers use it, so the guard is defined in one place.
  • Claude Code (.claude/settings.json): buildHookEntry writes the guarded command. addHookEntry now treats an entry as up to date only if both its matcher and its Chainloop command match. Previously it compared only the matcher, so an unguarded entry from an earlier trace init would never have been upgraded. A small chainloopHookCommand helper finds the Chainloop handler in a group.
  • Cursor (.cursor/hooks.json): buildHookEntry writes the guarded command, and addHookEntry compares against it. Cursor's flat entry format is unchanged.
  • Unchanged as the issue asks: user hooks are untouched, trace init stays idempotent, and the Git hooks and the pre-push requireTrace enforcement are untouched. exec passes stdin and stdout through and keeps the CLI's exit status when it's present.

Tests, in both claude/hooks_test.go and cursor/hooks_test.go:

  • TestInstalledHooksWithoutCLI runs every hook command with PATH holding only sh. Each one exits 0 with no output, and every installed command contains the guard.
  • TestInstalledHookPreservesIOAndExitStatus runs against a stub chainloop that echoes its arguments and stdin and exits with 0 or 37. The output and exit code come through unchanged.
  • TestInstallUpgradesUnguardedHooks starts from a settings file with a bare chainloop trace hook … post-tool-use entry next to a user hook. InstallHooks replaces it with exactly one guarded entry, keeps the user hook, and a second run changes nothing.
  • The existing command assertions now expect the guarded form.

go test ./app/cli/internal/trace/..., go vet and gofmt -l are clean (Go 1.27.3, offline). Against main's hooks.go the new install and upgrade tests fail. The runtime tests skip on Windows, as Codex's do.

One question: Codex also sets a commandWindows with a cmd.exe/where guard. As far as I know Claude Code needs Git Bash on Windows, so sh should be available there, but I couldn't check it on Windows. I don't know of an equivalent override field for Cursor, so Cursor on Windows needs sh on PATH. If that's a concern, I can make the Cursor guard platform-specific at install time.