Skip to content

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

Open
bercedev wants to merge 4 commits into
chainloop-dev:mainfrom
bercedev:fix/3687-guard-agent-hooks
Open

bercedev wants to merge 4 commits into
chainloop-dev:mainfrom
bercedev:fix/3687-guard-agent-hooks

Conversation

@bercedev

@bercedev bercedev commented Oct 21, 2026 •

Copy link
Copy Markdown

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.

… is missing

The generated hook commands called chainloop directly, so opening a repository with the hooks installed but no CLI on PATH failed every hook with exit 127. They are now wrapped in a command -v guard that exits 0 when the CLI is missing and execs it otherwise, as the Codex hooks do. Re-running chainloop trace init upgrades unguarded entries.

Signed-off-by: berce <75228263+bercedev@users.noreply.github.com>
@chainloop-platform

chainloop-platform Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

PR validation — ⚠️ 1 failing

Status Policy Material Messages
⚠️ Failed pr-min-approvals pr-info PR/MR #3691 has 0 approving reviews, 1 required.
✅ Passed pr-description-required pr-info -
✅ Passed pr-user-story-linked pr-info -

View attestation ↗

AI Session Checks — ⚠️ no AI session found

Missing AI Coding Sessions

This organization requires every PR to be backed by a Chainloop Trace AI coding session, and none was found for this one.

Please make sure the AI coding session evidence has been sent by the Chainloop CLI, or add the skip-ai-session label to this PR to bypass this check.

Learn more about Chainloop Trace.

Security Checks — ✅ 5 passing

✅ secret-scan

Status Policy Messages
✅ Passed secrets-detection -

✅ sast-scan

Status Policy Messages
✅ Passed owasp-top10-2025 -
✅ Passed sast -
✅ Passed cwe-top25 -
✅ Passed cwe-top26-40-cusp -
Scans not applied (3)
Scan Reason
vulnerability-scan no manifest/lockfile changed
github-actions-scan no workflow files changed
iac-scan no IaC files changed

View attestation ↗

Security context

✅ Nothing this change touches has a recorded security-fix history.

View in Chainloop ↗ · How this works ↗


Powered by Chainloop and Chainloop Trace

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/cli/internal/trace/provider.go">

<violation number="1" location="app/cli/internal/trace/provider.go:404">
P2: The shared wrapper hard-codes POSIX `sh`, so Claude and Cursor hooks fail on Windows installations without Git Bash instead of skipping cleanly. Generate a Windows-compatible guard alongside the POSIX command, or use each provider’s platform-specific command fields.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread app/cli/internal/trace/provider.go
Comment thread app/cli/internal/trace/claude/hooks.go Outdated
Comment thread app/cli/internal/trace/provider.go
…grading hooks

Cursor blocks the shell command when a preToolUse hook writes no valid response, so the missing-CLI guard of that hook now prints the same {"permission":"allow"} as AllowToolUse (new GuardHookCommandWithOutput). Upgrading an unguarded Claude hook replaced its whole group and dropped any user handler in it; the Chainloop handler is now updated in place, or moved to a group of its own when its matcher changed.

Signed-off-by: berce <75228263+bercedev@users.noreply.github.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread app/cli/internal/trace/claude/hooks.go
removeHookEntry dropped every group that held a Chainloop handler, together with any user handler in it. It now strips only the Chainloop handler and drops the group when nothing else is left.

Signed-off-by: berce <75228263+bercedev@users.noreply.github.com>
Comment thread app/cli/internal/trace/provider.go
…kCommand

The Codex provider kept its own copy of the guard's shell string. It now uses trace.GuardHookCommand, like the Claude Code and Cursor providers, and derives the prefix it strips when recognising its handler from the same function. The Windows prefix stays in the Codex package, since only Codex uses it.

Signed-off-by: berce <75228263+bercedev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(trace): skip agent hooks when the Chainloop CLI is unavailable

2 participants