Skip to content

feat(trace): open source the chainloop trace command - #3388

Merged
jiparis merged 7 commits into
chainloop-dev:mainfrom
jiparis:feat/chainloop-trace
Aug 31, 2026
Merged

jiparis merged 7 commits into
chainloop-dev:mainfrom
jiparis:feat/chainloop-trace

Conversation

@jiparis

@jiparis jiparis commented Aug 31, 2026 •

Copy link
Copy Markdown
Member

Brings chainloop trace — AI coding session capture and attestation — into the OSS CLI. Until now it shipped only in the enterprise CLI, even though the CHAINLOOP_AI_CODING_SESSION evidence 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 init installs 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/, and git push attests the session as aicodingsession evidence. chainloop trace run -- <cmd> does the same for a single-shot agent invocation and propagates the wrapped command's exit code. chainloop trace uninstall removes everything it installed.

This repo has been dogfooding the command through the enterprise CLI already — see .chainloop.yml, .claude/settings.json and devel/sandbox-kit/.

Layout

Path Contents
app/cli/internal/trace/** transcript parsing per agent, line attribution, git state, hook installation, .chainloop.yml handling
app/cli/pkg/action/trace_*.go TraceRun, RunTracePush, the agent hook handlers, AttestationExecutor, SubprocessExitError
app/cli/cmd/trace*.go the command tree, plus the hook logger

Commits 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

AttestationExecutor takes its dependencies as parameters. The enterprise copy read cmd.ActionOpts and cmd.Version directly, which is an import cycle here since cmd imports pkg/action. Both are now passed in by the caller, with ActionOpts/CLIVersion fields on TraceRunOpts and RunTracePushOpts. Note cliVersion must be the bare cmd.Version and not ActionsOpts.CLIVersion — the latter is fullVersion(), which appends the edition and would end up in the attestation predicate.

The hook logger lives in cmd, not pkg/action. Hook commands swap the root logger for a colorless, level-filtered one that also writes to the trace state's log.txt, because their output interleaves with git's during a commit or push. cmd already owns that logger var, so trace_hooklog.go operates on it and adds only a cmd.Logger() getter. Putting a mutable exported Logger in pkg/action would have added a third logger source to a package other projects import.

main gained a SubprocessExitError case, first in the switch. Without it trace run always exits 1 regardless of what the wrapped command did.

Two test adjustments worth a look. TestGoGitClient_GeneratedMatcherProjectAttributes runs the generated-file matcher against the real repository .gitattributes, so its expectations were retargeted at this repo's paths — taken from git check-attr linguist-generated, and still covering the "last match wins" override (ent/** generated, then ent/migrate/** and ent/schema/* not). And initGitRepo now resolves symlinks on its temp dir, without which three attribution tests fail on macOS where t.TempDir() returns /var/folders/... but the repo root resolves to /private/var/folders/....

Dependencies: no new modules. go mod tidy promotes go-git/go-billy/v6 and sergi/go-diff from indirect to direct at the versions already in the graph; go.sum is unchanged.

Verification

  • make -C app/cli test green; golangci-lint run ./app/cli/... reports nothing in any added file
  • go mod tidy and the CLI reference regeneration are both idempotent
  • go vet -tags integration ./app/cli/internal/trace/opencode/ — CI never compiles that file
  • Against a built binary in a scratch repo: trace init writes executable hooks, a commit fires them, trace hook claude session-start runs without contacting the control plane and logs to log.txt rather than stderr, --debug lowers the stderr floor, trace run -- sh -c 'exit 42' exits 42, and trace uninstall leaves no residue
  • trace hook is hidden, so it stays out of --help and out of cli-reference.mdx

Review in cubic

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>
@jiparis
jiparis requested a review from a team August 31, 2026 14:47
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>

@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

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

Re-trigger cubic

Comment thread app/cli/pkg/action/trace_run.go
Comment thread app/cli/internal/trace/hooks/hooks.go
Comment thread app/cli/internal/trace/hooks/hooks.go
Comment thread app/cli/internal/trace/git/gogit.go
Comment thread app/cli/internal/trace/hooks/hooks.go
Comment thread app/cli/internal/trace/git/exec.go
Comment thread app/cli/internal/trace/state/state.go
Comment thread app/cli/internal/trace/cursor/provider.go
Comment thread app/cli/internal/trace/config/config.go
Comment thread app/cli/main.go
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
@chainloop-platform

chainloop-platform Bot commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

AI Session Checks — 🟢 91% · ⚠️ 1 failing

Avg score Sessions Failing policies Attribution Files Lines Total Duration
🟢 91% 1 ⚠️ 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)

View session details ↗

Change Summary

  • Seeds the package logger in NewRootCmd so Cobra pre-hook errors are emitted.
  • Adds TestNewRootCmdInitializesLogger as a regression test.
  • Leaves main.go unchanged and fixes the root cause in cmd/root.go.

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% Human

Status 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-allowed ai-coding-session-f4cb9d -
✅ Passed ai-config-no-dangerous-commands ai-coding-session-f4cb9d -
⚠️ Failed ai-config-no-secrets ai-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-allowed ai-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 -
⚠️ Failed 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

View attestation ↗


Powered by Chainloop and Chainloop Trace

@jiparis
jiparis marked this pull request as ready for review August 31, 2026 15:32

@migmartri migmartri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🎉

@jiparis
jiparis merged commit 5e57a62 into chainloop-dev:main Aug 31, 2026
16 of 17 checks passed
@jiparis
jiparis deleted the feat/chainloop-trace branch August 31, 2026 19:14
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.

2 participants