Repository navigation
feat(trace): open source the chainloop trace command - #3388
Merged
Merged
Conversation
First step of opening `chainloop trace`. The whole subtree moves from the closed-source platform repo unchanged apart from the module prefix: it only ever depended on OSS primitives (pkg/attestation/crafter/materials/ aicodingsession) and third-party libraries. Two adjustments were unavoidable: - TestGoGitClient_GeneratedMatcherProjectAttributes runs the matcher against the real repository .gitattributes, so its expectations were retargeted at this repo's paths. The new expectations were taken from `git check-attr linguist-generated`, and they still exercise the "last match wins" override (ent/** generated, then ent/migrate/** and ent/schema/* not). - Two comments pointed at platform paths that no longer resolve. The cmd and action layers follow in the next commits; nothing references this code yet. Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
Ports TraceRun, RunTracePush, the agent hook handlers and the
AttestationExecutor, plus SubprocessExitError, which the CLI needs in a
non-internal package so the exit code of a wrapped `trace run` command
can be recognised from main.
The platform copy read cmd.ActionOpts and cmd.Version directly, which
would be an import cycle here (cmd already imports pkg/action). Both are
now passed in by the caller:
NewAttestationExecutor(base *ActionsOpts, cliVersion string, opts ...)
with ActionOpts/CLIVersion fields added to TraceRunOpts and
RunTracePushOpts, and HandlePrePushHook taking a RunTracePushOpts so the
pre-push hook can forward them. Note cliVersion must be the bare
cmd.Version: ActionsOpts.CLIVersion is fullVersion(), which appends the
edition and would change the recorded attestation predicate.
Test adjustments:
- the platform package-level Logger becomes zerolog.Nop(), matching the
convention in attestation_init_test.go
- initGitRepo resolves symlinks on the temp dir. Without it the three
post-tool-use attribution tests fail on macOS, where t.TempDir()
returns /var/folders/... but the repo root resolves to
/private/var/folders/..., so the repo-relative paths under test come
out as "../.." escapes. These were already failing in the platform
repo; Linux CI hid it.
Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
Registers `chainloop trace` (init, run, uninstall, and the hidden hook tree) on the root command, completing the port. The hook logger moves here as trace_hooklog.go rather than into pkg/action, where the platform copy lived. Hook commands need to swap the root logger for a colorless, level-filtered one whose output also lands in the trace state's log.txt, and this package already owns that logger var. Exporting a mutable Logger from pkg/action would have added a third competing logger source to a package other projects import. SetLogger/SetLogLevel/DefaultLogger are dropped: --debug now lowers the stderr floor directly via flagDebug. main gains the SubprocessExitError case, first in the switch, so `chainloop trace run -- <cmd>` exits with the wrapped command's status instead of always 1. It also reads cmd.Logger() for the final error line, so that line matches the hook's colorless formatting when a hook fails mid-commit or mid-push. Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
…eference - Apache-2.0 headers on all 84 ported files, as goheader requires. Build tags stay above the header. - go mod tidy promotes go-git/go-billy/v6 and sergi/go-diff from indirect to direct at the versions already in the graph. No new modules and no go.sum change. - cli-reference.mdx gains trace, trace init, trace run and trace uninstall. The hook subcommands are Hidden and stay out, as intended. `golangci-lint run ./app/cli/...` reports no findings in any ported file. Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
The hooks ran but said nothing, so they looked like they were not firing at all. `stderrMinLevel` defaulted to Warn and was only lowered by --debug, which filtered out every Info-level message the hooks emit — "commit record saved" on post-commit, the attestation progress on pre-push. Those are the only signal a user gets that trace is working inside a git commit or push, and the hook wrapper ends in `exit 0`, so nothing surfaced. The enterprise CLI looked like it had the same Warn default, but its root command called SetLogLevel(InfoLevel) on every invocation, and that lowered the floor to Info because Info < Warn. So the shipped behaviour was always Info; the Warn default was dead. Dropping SetLogLevel in the port silently changed it. Now mirrors initLogger: Info normally, Debug with --debug. The on-disk log.txt still receives every level either way. Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
Contributor
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Cobra rejects unknown subcommands and invalid flags before PersistentPreRunE runs, so the package-level logger was still its zero value when main printed those errors through cmd.Logger(). A zero-value zerolog.Logger has no writer, and with SilenceErrors set main is the only place the message is emitted, so users got a silent non-zero exit. Seed the logger from NewRootCmd so it is usable as soon as the command tree exists. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: f4cb9dc0-9116-40de-8c9a-56b23ae2f406
Contributor
AI Session Checks — 🟢 91% ·
|
| Avg score | Sessions | Failing policies | Attribution | Files | Lines | Total Duration |
|---|---|---|---|---|---|---|
| 🟢 91% | 1 | 100% AI / 0% Human | 2 | +24 / -0 | 15m28s |
🟢 91% — 100% AI — ⚠️ 1 policies failing
-
Aug 31, 2026 15:16 UTC · 15m28s · $4.38 · 118 in / 28.1k out · claude-code 2.1.251 (claude-opus-5)
Change Summary
-
- Seeds the package logger in
NewRootCmdso Cobra pre-hook errors are emitted. - Adds
TestNewRootCmdInitializesLoggeras a regression test. - Leaves
main.gounchanged and fixes the root cause incmd/root.go.
- Seeds the package logger in
AI Session Overall Score
-
🟢 91% — Clean session with strong verification and a focused, well-aligned fix.
AI Session Analysis Breakdown
-
🟢 96% · verification
-
🟢 The AI added a failing test before touching the implementation. · High Impact
🟢 95% · scope-discipline
-
🟢 The landed change stayed to two files directly tied to the fix. · High Impact
🟢 92% · alignment
-
No notes.
🟢 90% · solution-quality
-
No notes.
🟢 86% · context-and-planning
-
No notes.
🟢 82% · user-trust-signal
-
🟢 The user moved straight to commit and push after the fix summary. · High Impact
-
File Attribution
████████████████████100% AI / 0% HumanStatus Attribution File Lines modified ai app/cli/cmd/root_test.go+18 / -0 modified ai app/cli/cmd/root.go+6 / -0
Policies (4, 1 failing)
Status Policy Material Messages ✅ Passed ai-config-ai-agents-allowedai-coding-session-f4cb9d- ✅ Passed ai-config-no-dangerous-commandsai-coding-session-f4cb9d- ⚠️ Failedai-config-no-secretsai-coding-session-f4cb9d- Potential secret (Quoted API key/password) found in session content [turn=75, source=tool_result, line=21, value=Token = ...uth"]
- Potential secret (Quoted API key/password) found in session content [turn=75, source=tool_result, line=23, value=Token = ...ken"]
✅ Passed ai-config-mcp-servers-allowedai-coding-session-f4cb9d- -
Security Checks — ⚠️ 1 failing
✅ 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 |
- |
✅ iac-scan
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | iac-misconfiguration |
- |
PR info
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | pr-description-required |
- |
pr-user-story-linked |
PR/MR #3388 does not reference a user story or issue in title, description, or branch 'feat(trace): open source the chainloop trace command'. Expected patterns: ["(?i)[A-Z]+-[0-9]+", "#[0-9]+", "(?i)[A-Z]{2", "}-[0-9]+", "(?i)gh-[0-9]+", "(?i)\[[A-Z]+-[0-9]+\]"] |
⏭️ 2 scans not applied
| Scan | Reason |
|---|---|
vulnerability-scan |
ran, but its output is not attested yet |
github-actions-scan |
no workflow files changed |
Powered by Chainloop and Chainloop Trace
jiparis
marked this pull request as ready for review
August 31, 2026 15:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings
chainloop trace— AI coding session capture and attestation — into the OSS CLI. Until now it shipped only in the enterprise CLI, even though theCHAINLOOP_AI_CODING_SESSIONevidence type, its crafter, its schema and its secret redaction have all been here for a while. This adds the producer side.See the docs already available at: https://docs.chainloop.dev/guides/chainloop-trace#how-to-trace-ai-coding-sessions
See comment below for the session summary that belongs to this PR and has been attested using a binary built from this branch.
chainloop trace initinstalls git hooks plus agent hooks for Claude Code, Cursor and opencode. From then on, commits are annotated with the sessions that produced them, per-file and per-line AI attribution is recorded under.git/chainloop-trace/, andgit pushattests the session asaicodingsessionevidence.chainloop trace run -- <cmd>does the same for a single-shot agent invocation and propagates the wrapped command's exit code.chainloop trace uninstallremoves everything it installed.This repo has been dogfooding the command through the enterprise CLI already — see
.chainloop.yml,.claude/settings.jsonanddevel/sandbox-kit/.Layout
app/cli/internal/trace/**.chainloop.ymlhandlingapp/cli/pkg/action/trace_*.goTraceRun,RunTracePush, the agent hook handlers,AttestationExecutor,SubprocessExitErrorapp/cli/cmd/trace*.goCommits are split so the bulk lands as a reviewable move: the internals arrive unchanged apart from the import prefix, then the action layer, then the command layer and wire-up, then license headers and generated docs.
Notes for review
AttestationExecutortakes its dependencies as parameters. The enterprise copy readcmd.ActionOptsandcmd.Versiondirectly, which is an import cycle here sincecmdimportspkg/action. Both are now passed in by the caller, withActionOpts/CLIVersionfields onTraceRunOptsandRunTracePushOpts. NotecliVersionmust be the barecmd.Versionand notActionsOpts.CLIVersion— the latter isfullVersion(), which appends the edition and would end up in the attestation predicate.The hook logger lives in
cmd, notpkg/action. Hook commands swap the root logger for a colorless, level-filtered one that also writes to the trace state'slog.txt, because their output interleaves with git's during a commit or push.cmdalready owns that logger var, sotrace_hooklog.gooperates on it and adds only acmd.Logger()getter. Putting a mutable exportedLoggerinpkg/actionwould have added a third logger source to a package other projects import.maingained aSubprocessExitErrorcase, first in the switch. Without ittrace runalways exits 1 regardless of what the wrapped command did.Two test adjustments worth a look.
TestGoGitClient_GeneratedMatcherProjectAttributesruns the generated-file matcher against the real repository.gitattributes, so its expectations were retargeted at this repo's paths — taken fromgit check-attr linguist-generated, and still covering the "last match wins" override (ent/**generated, thenent/migrate/**andent/schema/*not). AndinitGitReponow resolves symlinks on its temp dir, without which three attribution tests fail on macOS wheret.TempDir()returns/var/folders/...but the repo root resolves to/private/var/folders/....Dependencies: no new modules.
go mod tidypromotesgo-git/go-billy/v6andsergi/go-difffrom indirect to direct at the versions already in the graph;go.sumis unchanged.Verification
make -C app/cli testgreen;golangci-lint run ./app/cli/...reports nothing in any added filego mod tidyand the CLI reference regeneration are both idempotentgo vet -tags integration ./app/cli/internal/trace/opencode/— CI never compiles that filetrace initwrites executable hooks, a commit fires them,trace hook claude session-startruns without contacting the control plane and logs tolog.txtrather than stderr,--debuglowers the stderr floor,trace run -- sh -c 'exit 42'exits 42, andtrace uninstallleaves no residuetrace hookis hidden, so it stays out of--helpand out ofcli-reference.mdx