Repository navigation
feat(trace): local storage mode for AI coding sessions - #3617
Conversation
PR validation — ✅ 3 passing
AI Session Checks — 🟢 89% · ✅ 0 failing
|
| Status | Policy | Material | Messages |
|---|---|---|---|
| ✅ Passed | ai-config-ai-agents-allowed |
ai-coding-session-ac2618 |
- |
| ✅ Passed | ai-config-no-dangerous-commands |
ai-coding-session-ac2618 |
- |
| ✅ Passed | ai-config-no-secrets |
ai-coding-session-ac2618 |
- |
| ✅ Passed | ai-config-mcp-servers-allowed |
ai-coding-session-ac2618 |
- |
🟢 93% — 100% AI — ✅ All policies passing
-
Oct 14, 2026 02:52 UTC · 2h3m24s · $118.09 · 1.2k in / 768.3k out · claude-code 2.1.296 (claude-opus-4-8)
Change Summary
-
- Adds
traceStorage,tracePath, andtraceRequiredplumbing for local session storage. - Implements local-mode storage across
trace init, pre-push hooks,trace run, and session-start banners. - Adds focused tests, updates CLI docs/Trace README, and preserves stored sessions across cleanup.
- Adds
AI Session Overall Score
-
🟢 93% — Clean, well-planned feature work with strong verification and no visible trust erosion.
AI Session Analysis Breakdown
-
🟢 95% · alignment
-
🟢 The AI resolved spec ambiguities with the user before coding and then stayed on that path. · High Impact
🟢 95% · context-and-planning
-
🟢 A detailed plan landed before editing and framed the whole implementation. · High Impact
🟢 95% · verification
-
🟢 Focused tests and two manual binary checks covered the new local-mode paths. · High Impact
🟢 93% · user-trust-signal
-
No notes.
🟢 90% · scope-discipline
-
No notes.
🟢 89% · solution-quality
-
No notes.
-
File Attribution
████████████████████100% AI / 0% Human
Policies (4)
Status Policy Material Messages ✅ Passed ai-config-ai-agents-allowedai-coding-session-7492b7- ✅ Passed ai-config-no-dangerous-commandsai-coding-session-7492b7- ✅ Passed ai-config-no-secretsai-coding-session-7492b7- ✅ Passed ai-config-mcp-servers-allowedai-coding-session-7492b7- -
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 |
Security context
This change touches code with 8 recorded security-fix advisories. These are pointers to what past fixes established, not findings in this diff, and they never fail the check.
View in Chainloop ↗ · How this works ↗
Powered by Chainloop and Chainloop Trace
…ontrol plane Chainloop Trace records an AI coding session and, on git push, uploads a signed attestation to a control plane. This adds a local mode where a repository records its sessions and stores the assembled evidence set on disk, with no control plane, login, or network. The storage mode is a committed repository policy in .chainloop.yml: traceStorage (CONTROL_PLANE default, or LOCAL), an optional tracePath, and traceRequired (replacing the deprecated requireTrace). `trace init --local` sets a repository up with no control plane, and the pre-push hook then runs the same push-time assembly and writes each material under its content digest, with a per-session manifest that declares the output unsigned and not an attestation. `trace run` honors the mode as well. The stored evidence equals what a push would upload apart from the signature, and secret redaction stays on by default with a CHAINLOOP_TRACE_NO_REDACT opt-out for a trusted local store. In local mode no control-plane connection is opened: the trace commands build it lazily and only on the attestation path. The stored sessions are kept across a trace uninstall. Refs #3582 Chainloop-Trace-Sessions: 7492b77a-1dc6-4352-823f-f87c42672452 Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
7be3fc4 to
49b0b6c
Compare
The local store sized the merged annotations map from len(crafted)+len(extra), which a static analyzer flags as an allocation size that may overflow. Both maps hold only a handful of annotation keys, so the sum cannot overflow in practice; size the hint from one length and let the map grow instead. Refs #3582 Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
There was a problem hiding this comment.
All reported issues were addressed across 23 files
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
The local-mode session-start banner now says the session is recorded in local mode, so a developer can tell at a glance which storage mode is active without inferring it from the destination line (R-012). Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
A config read or parse error no longer selects CONTROL_PLANE silently, so a LOCAL repository with a broken .chainloop.yml cannot have its pre-push hook upload a local-only session. Local evidence is finalized only when every gathered session is stored, so a partial failure no longer strands a session's retry state. A control-plane trace run is reported in telemetry again, while a local run stays excluded. A trace run that stores its evidence surfaces a store failure when the wrapped command succeeded. Uninstall preserves a configured tracePath that resolves inside the trace directory. The local session-start banner states the storage timing that matches how the session is driven. Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
…clone guidance A control-plane trace init over a repository previously set to LOCAL now writes traceStorage: CONTROL_PLANE, so the pre-push hook stops storing on disk and the init no longer disagrees with how the repository behaves. The local-mode next steps now tell each teammate to run chainloop trace init --local after cloning, since the managed git hooks live under .git and are not cloned, so a commit alone does not set a clone up to record. Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
A control-plane trace init now surfaces an invalid traceStorage in an existing .chainloop.yml before installing hooks, instead of discarding the error and leaving a repository whose every pre-push fails on the same config. An explicit --local run still overrides the bad value. Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
…ent it A control-plane trace init that clears a previous LOCAL policy now tells the user the repository switched and to commit .chainloop.yml, instead of changing the mode with only a debug log. The trace README documents how to switch storage modes in both directions and that each clone installs its own hooks. Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
I'd NOT make it imply local since in the future we might allow more than one mode I'd also like to discuss the right default location, would it be or should it be |
Maybe here we can add, if in the future you wna to connect this repo to a chainloop control plane, run trace init again |
migmartri
left a comment
There was a problem hiding this comment.
@matiasinsaurralde ptal at my comments, I can't make it work as well as I added some additional feedback
I think |
Replace the single traceStorage value and the --local flag with a traceModes list in .chainloop.yml and the --online and --offline flags of trace init. A repository can attest to the control plane, store sessions on disk, or do both. With both modes, the pre-push hook stores the offline copy first, then attests. A failed mode keeps the sessions for the next push. - trace init with no mode flag keeps the modes in .chainloop.yml. - --trace-path no longer turns on the offline mode. - An online init that fails on authentication, or cannot reach the control plane, suggests "chainloop trace init --offline". - The session-start banner names the active trace modes. - A clone of an offline repository needs no trace init: the first traced session installs the git hooks, as in the online mode. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
There was a problem hiding this comment.
All reported issues were addressed across 23 files (changes from recent commits).
Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack
View guided diff | Re-trigger cubic
…ther" This reverts commit db6b419. Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Assisted-by: Claude Code
There was a problem hiding this comment.
All reported issues were addressed across 23 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
A clone of a local-mode repository needs no trace init: the first traced session installs the git hooks, as in control-plane mode. Remove the next-steps text that told each teammate to run init after cloning, fix the README to match, and test the auto-install for a local-mode clone with no project name. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
Rewrite the help, flag descriptions, init output, errors, logs and the session-start banner of the local mode with short, active sentences and no semicolons. Remove "the push tells you where" from the next steps. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
There was a problem hiding this comment.
All reported issues were addressed across 11 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
When a control-plane trace init fails because the user is not authenticated, add a tip to the error: run "chainloop trace init --local" to configure the repository in local mode. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
--trace-path no longer turns on local mode. tracePath in .chainloop.yml now sets the directory where Chainloop Trace keeps its data, in local and control-plane mode. The default stays .git/chainloop-trace, and a relative path starts at the repository root. Local mode writes the sessions to the local-sessions folder of that directory. Every git store of a repository now comes from state.NewRepoStore, so the git hooks, the agent hooks, the pre-push hook, trace run and trace uninstall all use the same directory. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
- A local store that skips a session now returns an error, so the pre-push hook warns, or blocks the push when traceRequired is set. - trace init help: the workflow and the login apply to control-plane mode only. - trace run help: the identity flags are required in control-plane mode only, trace run also reads tracePath, and the workflow is created in control-plane mode only. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
trace run takes its settings from its flags, so local mode is now the --local flag instead of traceStorage in .chainloop.yml. With --local, the control-plane flags (--org, --project, --workflow, --version, --contract) are rejected. The run marks itself local in the trace state, so the session-start banner shows the mode of the run. trace run still uses tracePath when the repository sets one, because the agent hooks keep the session there. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
|
I took over this PR to address the review. These are the changes since 95c1353:
Local mode tip for authentication errors (69e74fc)
cubic findings (33bda24)
Text (020af50)
Open item
AI disclosure: these changes were written with Claude Code. |
|
LGTM, ptal at the readme Did you make any changes in teh manifest? |
…race-local-mode Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> # Conflicts: # app/cli/internal/trace/README.md
Not yet, although it has all the metadata for materials. It'd be good to have also the common metadata (commit information, etc), as in attestations. I'll take a look how far are we from that: |
Replace the custom manifest.json of a locally stored session with statement.json: the in-toto statement a push would sign, with the chainloop.dev/attestation/v0.2 predicate. It is rendered from an in-memory crafting state with the Chainloop renderer, with no envelope and no signature. The disk sink keeps each crafted material as a push keeps it in its crafting state, without inline content, since the bytes are on disk under their digest. The statement subject names the ai-coding-session workflow and the git head commit. Add crafter.GitRepoHead to read the head commit without platform verification. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
Add Decision Record rows D-021 to D-024 to spec issue-3582: the unsigned in-toto statement as the session index, tracePath as the trace directory in every mode, the --local flag of trace run, and no trace init for a clone of a local-mode repository. Refs #3582 Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
|
New changes since the last summary:
Spec (5cba713)
I also updated the PR description to match the current design. AI disclosure: these changes were written with Claude Code. |
Fix the goconst findings of the lint jobs in the trace init and trace run flag tests. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863
|
@jiparis let me know once this is ready for the final test? |
you can go ahead |
|
Example statement in local mode: {
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "chainloop.workflow.ai-coding-session",
"digest": {
"sha256": "fdf00c7945dfd090ae6684864da956d64344be38ed34824682cfaeeadd1a0d69"
}
},
{
"name": "git.head",
"digest": {
"sha1": "c314cc40f97a8da85237bb92fa4e2b9db35036cb"
},
"annotations": {
"author.email": "jiparis@chainloop.dev",
"author.name": "Jose I. Paris",
"date": "2026-10-15T15:50:15Z",
"message": "Add second greeting to README\n\nCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>\n\nChainloop-Trace-Sessions: bdb9d8ef-ed14-4471-b72a-afe27af4c2b4\n",
"remotes": [
{
"name": "origin",
"url": "git@github.com:jiparis/workflow-tests.git"
}
],
"signature": "-----BEGIN PGP SIGNATURE-----\n\niQJPBAABCAA5FiEEjH51HMWkCNY2dhZwoItyCk+m3t4FAmrQ9jcbFIAAAAAABAAO\nbWFudTIsMi41KzEuMTIsMCwzAAoJEKCLcgpPpt7eUIAP+gOTT8UjUub89qbxZQ1k\nRJsGS7YKyFzl2MLm+nFNWwzdJO5Zf58Gl1N3ktilJbBfIIDFcy9/u1m01erv7K8X\nMWRcZ5K4T9X8tO94iZ8JUw9hS9RvuC0QH1rp7By7JFM1WvGr41KkN4N6FzYS95h0\nCEPXihZOE0z4SdZER4WLJnwC7rChCJwkijQjl6efIUa3dQeG+/YDQ9u1stJs9E6d\nAXRypkxNksbmh2KEJo3w2+kDZo1COfO/xAa3vlaD4UZkiLg8bHwH09WjV1c73Qbt\nDihuE3OOhldLf0dvkIOoZG+qdb91ERnnLSdvOefxy9traME9hxCJ7rh3n+aUCk0J\nNG2qA1jP/ulAowq0v1DSeCihZEsFIUuZOksgNSNtxPehCCnM97FbmuUaf7doNKDC\nnAxbWR9bVGikAo/o6wZse+fWUT5WQ5SsSLK/2mZl/JgjdzbIK52esjAPSj0k4pec\nyTRBFKb9LE28VIKfmP/xjvWt6PWdPpcrfmU1MebuO0VMniLkHiJAX10neQv+PO9L\nkQVytyvOf73u1UobndBbyWnUXyDWUoVnFEuq/DrCzPz/C6O8PGd2yAhvLvP0MUoR\nYfkA3HBYMfCCUtHm+PVlMd4GzgjuUAKfbZ4I58pjGoKmNhB2hcTbAyYoopbQBIEA\nbd32j83l6cl6Oj9KUcqdRKmR\n=/OF3\n-----END PGP SIGNATURE-----\n",
"signature.algorithm": "PGP"
}
}
],
"predicateType": "chainloop.dev/attestation/v0.2",
"predicate": {
"buildType": "chainloop.dev/workflowrun/v0.1",
"builder": {
"id": "chainloop.dev/cli/dev@"
},
"materials": [
{
"annotations": {
"chainloop.material.aiagent.model": "claude-opus-5-5",
"chainloop.material.aiagent.name": "claude-code",
"chainloop.material.aisession.mode": "coding",
"chainloop.material.name": "ai-coding-session-bdb9d8",
"chainloop.material.redacted": "true",
"chainloop.material.redaction.count": "0",
"chainloop.material.redaction.ruleset": "3",
"chainloop.material.size": "819301",
"chainloop.material.type": "CHAINLOOP_AI_CODING_SESSION"
},
"digest": {
"sha256": "bd40bc76938d9a9ae955b5413f780746fb03348fbc2d6a28b8875b86ee5bc2f7"
},
"name": "chainloop-trace-270179962.json"
}
],
"metadata": {
"contractName": "",
"contractVersion": "",
"finishedAt": "2026-10-15T15:50:28.844154Z",
"initializedAt": "2026-10-15T15:50:28.749673Z",
"name": "ai-coding-session",
"organization": "",
"project": "",
"projectVersion": "",
"projectVersionPrerelease": false,
"team": "",
"workflowID": "",
"workflowName": "ai-coding-session",
"workflowRunID": ""
},
"policyAttBlocked": false,
"policyBlockBypassEnabled": false,
"policyCheckBlockingStrategy": "ADVISORY",
"policyEvaluationsCount": 0,
"policyHasViolations": false,
"policyViolationsCount": 0,
"runnerType": "RUNNER_TYPE_UNSPECIFIED"
}
} |
…vidence A locally stored session now keeps its content-addressed files under evidence/<digest> next to statement.json. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: ac261808-7f67-40a0-b901-d1d43b9dd863






Refs PFM-7675
Summary
Chainloop Trace records an AI coding session and attests it to a control plane on
git push. This PR adds a local storage mode. A repository records its sessions and stores the evidence on disk, with no control plane, login, or network.The mode is a committed repository policy in
.chainloop.yml:traceStorage:CONTROL_PLANE(the default) orLOCAL.tracePath: the trace directory, where Chainloop Trace keeps its data in every mode. The default is.git/chainloop-trace. A relative path starts at the root of the checkout.traceRequired: replaces the deprecatedrequireTrace.chainloop trace init --localsets up a repository with no control plane (R-008).--trace-pathsets the trace directory and does not turn on local mode.trace initis a one-time setup for each repository. In a clone, the first traced session installs the git hooks, so a teammate does not runtrace init.When a control-plane
trace initfails because the user is not authenticated, the error has a tip to runchainloop trace init --local.In local mode, the pre-push hook runs the same assembly as a push (R-002, R-009). It writes each material under its content digest in the
local-sessionsfolder of the trace directory (R-003). It also writesstatement.json, the in-toto statement that a push would sign, with thechainloop.dev/attestation/v0.2predicate. The statement has no envelope and no signature, so it is not an attestation (R-006). A failure to store a session blocks the push only whentraceRequiredis set.chainloop trace run --local -- <command>stores one wrapped session the same way.trace runtakes its settings from its flags and does not readtraceStorage.Secret redaction stays on by default, with a
CHAINLOOP_TRACE_NO_REDACTopt-out for a trusted local store (R-004). Local mode opens no control-plane connection (R-005). Atrace uninstallkeeps the stored sessions. The session-start message names local mode and the directory of the stored sessions (R-012).Implements the design in
docs/specs/issue-3582-trace-evidence-export.md. Decision Record rows D-021 to D-024 record the changes from the PR review:manifest.jsonindex.tracePathsets the trace directory in every mode.trace runhas a--localflag.trace init.Refs #3582
Flow
A control-plane init with no login:
AI disclosure: parts of this PR were written with Claude Code.