Skip to content

feat(trace): local storage mode for AI coding sessions - #3617

Merged
migmartri merged 20 commits into
mainfrom
matias/issue-3582-trace-local-mode
Oct 15, 2026
Merged

migmartri merged 20 commits into
mainfrom
matias/issue-3582-trace-local-mode

Conversation

@matiasinsaurralde

@matiasinsaurralde matiasinsaurralde commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

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) or LOCAL.
  • 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 deprecated requireTrace.

chainloop trace init --local sets up a repository with no control plane (R-008). --trace-path sets the trace directory and does not turn on local mode. trace init is a one-time setup for each repository. In a clone, the first traced session installs the git hooks, so a teammate does not run trace init.

When a control-plane trace init fails because the user is not authenticated, the error has a tip to run chainloop 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-sessions folder of the trace directory (R-003). It also writes statement.json, the in-toto statement that a push would sign, with the chainloop.dev/attestation/v0.2 predicate. 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 when traceRequired is set.

chainloop trace run --local -- <command> stores one wrapped session the same way. trace run takes its settings from its flags and does not read traceStorage.

Secret redaction stays on by default, with a CHAINLOOP_TRACE_NO_REDACT opt-out for a trusted local store (R-004). Local mode opens no control-plane connection (R-005). A trace uninstall keeps 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:

  • The statement replaces the manifest.json index.
  • tracePath sets the trace directory in every mode.
  • trace run has a --local flag.
  • A clone needs no trace init.

Refs #3582

Flow

  1. Set up the repository:
% chainloop trace init --local

Congratulations, your repository is initialized in local mode

  storage       local (no control plane)
  sessions      written to .git/chainloop-trace/local-sessions on each push
  harnesses     claude-code

What's next

  1. Commit these files. Every clone then uses local mode:
       git add .chainloop.yml .claude/settings.json
  2. Start claude-code and write some code
  3. Commit and push. The pre-push hook writes the session evidence to disk
  1. Start a session. The session-start message says:
Chainloop Trace is recording this session in local mode.
Chainloop Trace writes the session evidence to <repo>/.git/chainloop-trace/local-sessions when you push.
  1. Commit and push:
INF AI coding session evidence stored at <repo>/.git/chainloop-trace/local-sessions sessions=1
  1. The stored session:
.git/chainloop-trace/local-sessions/<session-id>/statement.json
.git/chainloop-trace/local-sessions/<session-id>/evidence/sha256-<digest>

A control-plane init with no login:

ERR authentication required, please run "chainloop auth login"
Tip: run "chainloop trace init --local" to configure it in local mode

AI disclosure: parts of this PR were written with Claude Code.

@chainloop-platform

chainloop-platform Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

PR validation — ✅ 3 passing

Status Policy Material Messages
✅ Passed pr-min-approvals pr-info -
✅ Passed pr-description-required pr-info -
✅ Passed pr-user-story-linked pr-info -

View attestation ↗

AI Session Checks — 🟢 89% · ✅ 0 failing

Avg score Sessions Failing policies Attribution Files Lines Total Duration
🟢 89% 2 ✅ 0 100% AI / 0% Human 36 +3869 / -1698 7h46m32s

🟢 85% — 100% AI — ✅ All policies passing

Oct 15, 2026 10:18 UTC · 5h43m8s · $46.51 · 948 in / 420.3k out · claude-code 2.1.300 (claude-opus-5-5)

View session details ↗

Change Summary

  • Makes trace init effectively one-time per repo, adds the local-mode auth tip, and tightens user-facing CLI text.
  • Makes tracePath the shared trace-state directory in every mode, adds trace run --local, and preserves teammate-clone workflows.
  • Replaces local manifest.json with per-session unsigned statement.json, updates the spec/PR docs, and fixes new lint findings.

AI Session Overall Score

🟢 85% — Broad, well-verified review work with one planning gap and one early reset.

AI Session Analysis Breakdown

🟢 92% · verification

🟢 Repeated tests plus fresh-clone, worktree, and real-session runs gave strong runtime coverage. · High Impact

🟢 87% · solution-quality

🟢 Local evidence now reuses the standard Chainloop statement format instead of a custom manifest. · High Impact

🟢 85% · alignment

No notes.

🟢 84% · scope-discipline

🟢 The AI pushed back on trace run --trace-path because hook plumbing would have widened the change. · Medium Impact

🟢 82% · user-trust-signal

🟡 The user reset the first large commit and PR description before resuming with incremental fixes. · Low Severity

🟡 68% · context-and-planning

🟠 This multi-phase session had strong framing but no written shared plan before broad edits began. · Medium Severity

💡 For multi-phase review work, write a short shared plan before editing so later pivots do not require rollbacks.


File Attribution

████████████████████ 100% AI / 0% Human

Status Attribution File Lines
modified ai app/cli/cmd/trace_init.go +283 / -219
modified ai app/cli/cmd/trace_init_local_test.go +85 / -249
modified ai app/cli/pkg/action/trace_local_store_test.go +239 / -80
created ai app/cli/cmd/trace_init_modes_test.go +290 / -0
modified ai app/cli/pkg/action/trace_local_store.go +86 / -174
modified ai app/cli/pkg/action/trace_hook_handler.go +149 / -68
created ai app/cli/internal/trace/config/modes_test.go +165 / -0
modified ai app/cli/internal/trace/config/storage_test.go +1 / -159
modified ai app/cli/documentation/cli-reference.md +86 / -61
modified ai app/cli/pkg/action/trace_agent_hook.go +92 / -52
modified ai app/cli/pkg/action/trace_run.go +62 / -66
modified ai app/cli/cmd/trace_run.go +72 / -55
modified ai app/cli/internal/trace/config/config.go +66 / -52
modified ai app/cli/internal/trace/state/state.go +47 / -62
modified ai app/cli/internal/trace/state/state_test.go +82 / -23
modified ai app/cli/cmd/trace_run_local_test.go +79 / -3
modified ai app/cli/pkg/action/trace_agent_hook_test.go +66 / -3
modified ai pkg/attestation/crafter/crafter.go +36 / -16
modified ai app/cli/internal/trace/README.md +27 / -18
modified ai app/cli/internal/trace/state/store.go +43 / -2
modified ai pkg/attestation/crafter/crafter_unit_test.go +36 / -0
modified ai app/cli/cmd/trace_hook.go +18 / -16
modified ai app/cli/cmd/trace_init_test.go +18 / -5
modified ai app/cli/internal/repositoryconfig/config.go +11 / -8
modified ai app/cli/pkg/action/trace_banner_test.go +9 / -3

…and 5 more file(s).


Policies (4)

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)

View session details ↗

Change Summary

  • Adds traceStorage, tracePath, and traceRequired plumbing 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.

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

Status Attribution File Lines
created ai app/cli/pkg/action/trace_local_store.go +370 / -0
modified ai app/cli/pkg/action/trace_hook_handler.go +156 / -89
created ai app/cli/pkg/action/trace_local_store_test.go +206 / -0
modified ai app/cli/cmd/root.go +100 / -85
modified ai app/cli/cmd/trace_init.go +153 / -21
created ai app/cli/internal/trace/config/storage_test.go +145 / -0
created ai app/cli/cmd/trace_init_local_test.go +134 / -0
modified ai app/cli/internal/trace/config/config.go +97 / -0
modified ai app/cli/pkg/action/trace_run.go +70 / -20
modified ai app/cli/cmd/trace_run.go +58 / -27
modified ai app/cli/internal/trace/state/state.go +46 / -2
modified ai app/cli/documentation/cli-reference.md +28 / -15
modified ai app/cli/pkg/action/trace_agent_hook.go +31 / -7
modified ai app/cli/internal/trace/state/state_test.go +35 / -0
modified ai app/cli/cmd/trace_hook.go +24 / -1
modified ai app/cli/pkg/action/trace_spec_materials_test.go +12 / -12
modified ai app/cli/pkg/action/trace_spec_materials.go +15 / -4
modified ai app/cli/internal/repositoryconfig/config.go +16 / -1
modified ai app/cli/pkg/action/trace_skills_test.go +6 / -6
modified ai app/cli/internal/trace/README.md +3 / -2
modified ai app/cli/pkg/action/trace_pasted_images_test.go +2 / -2
modified ai app/cli/pkg/action/trace_spec_pointers_test.go +2 / -2
modified ai app/cli/pkg/action/trace_skills_scenario_test.go +1 / -1

Policies (4)

Status Policy Material Messages
✅ Passed ai-config-ai-agents-allowed ai-coding-session-7492b7 -
✅ Passed ai-config-no-dangerous-commands ai-coding-session-7492b7 -
✅ Passed ai-config-no-secrets ai-coding-session-7492b7 -
✅ Passed ai-config-mcp-servers-allowed ai-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

View attestation ↗

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

Comment thread app/cli/pkg/action/trace_local_store.go Fixed
…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>
@matiasinsaurralde
matiasinsaurralde force-pushed the matias/issue-3582-trace-local-mode branch from 7be3fc4 to 49b0b6c Compare October 14, 2026 15:07
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>
@matiasinsaurralde
matiasinsaurralde marked this pull request as ready for review October 14, 2026 18:23
@matiasinsaurralde
matiasinsaurralde requested a review from a team October 14, 2026 18:23

@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 23 files

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

View guided diff | Re-trigger cubic

Comment thread app/cli/internal/trace/config/config.go Outdated
Comment thread app/cli/pkg/action/trace_local_store.go Outdated
Comment thread app/cli/internal/trace/state/state.go
Comment thread app/cli/pkg/action/trace_run.go
Comment thread app/cli/cmd/trace_run.go
Comment thread app/cli/pkg/action/trace_agent_hook.go Outdated
Comment thread app/cli/documentation/cli-reference.md Outdated
Comment thread app/cli/cmd/trace_init.go Outdated
Comment thread app/cli/cmd/trace_init.go Outdated
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>

@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 2 files (changes from recent commits).

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

View guided diff | Re-trigger cubic

Comment thread app/cli/cmd/trace_init.go Outdated
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>
Comment thread app/cli/cmd/trace_init.go
Comment thread app/cli/pkg/action/trace_local_store.go
…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>
@migmartri

migmartri commented Oct 14, 2026 •

Copy link
Copy Markdown
Member
   --trace-path string   where local-mode sessions are stored (default: under .git/chainloop-trace/local-sessions); implies --local

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

(default: under .git/chainloop-trace/local-sessions);

or should it be .chainloop/... since that's what we do now with stored specs, etc?

@migmartri

Copy link
Copy Markdown
Member

Is this new?

image
Each teammate installs their own git hooks once after cloning, since the hooks
live under .git and are not cloned:
       chainloop trace init --local

what's that command for? isn't supposed to be already initialized

I'd also extend this error message saying that alternatively you can pass --local to do it offline.

chainloop trace init
ERR authentication required, please run "chainloop auth login"

in fact I am starting to think that that maybe the flag should be called --offline :)

@migmartri

Copy link
Copy Markdown
Member
Congratulations, your repository is initialized in local mode

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

Copy link
Copy Markdown
Member

Lastly, it's not working for me, after initialing,

image

I get this

diff --git a/.chainloop.yml b/.chainloop.yml
index 6c4097ad..9adc8a5a 100644
--- a/.chainloop.yml
+++ b/.chainloop.yml
@@ -5,10 +5,9 @@ projectName: chainloop
 # This indicates the [current version]+next
 # to indicate that we are building a new version of the project
 projectVersion: v1.123.0+next
-
 # Experimental feature used by Chainloop labs shared workflow https://github.com/chainloop-dev/labs
 # It maps the material names with location in disk so they get automatically attested
-
 scorecards:
   - name: sarif-results
     path: metadata/results.sarif
+traceStorage: LOCAL
diff --git a/.claude/settings.json b/.claude/settings.json
index 29a2edf6..ddbdce13 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -17,7 +17,7 @@
             "type": "command"
           }
         ],
-        "matcher": "Edit|Write|MultiEdit|Bash"
+        "matcher": "Edit|Write|MultiEdit|Bash|Skill"
       }
     ],
     "PostToolUseFailure": [
diff --git a/.opencode/plugins/chainloop-trace.ts b/.opencode/plugins/chainloop-trace.ts
index f23a9d01..2eb4e23d 100644
--- a/.opencode/plugins/chainloop-trace.ts
+++ b/.opencode/plugins/chainloop-trace.ts
@@ -2,6 +2,22 @@ import { spawn } from "node:child_process"

 const fileWritingTools = ["edit","write","apply_patch","patch"]
 const commandTools = ["bash","shell"]
+const skillTool = "skill"
+
+// skillDirFrom returns the folder of the skill that a skill tool call loaded:
+// the dir field of the result metadata, or else the base directory line of
+// the result text. Each OpenCode major gives the result in its own shape, so
+// each candidate is tried in turn.
+function skillDirFrom(...results: any[]): string {
+  for (const r of results) {
+    const dir = r?.metadata?.dir
+    if (typeof dir === "string" && dir) return dir
+    const text = typeof r === "string" ? r : typeof r?.output === "string" ? r.output : ""
+    const m = /^Base directory for this skill:\s*(.+)$/m.exec(text)
+    if (m) return m[1].trim()
+  }
+  return ""
+}

 function filePathsFromArgs(args: any): string[] {
   if (args?.filePath) return [args.filePath]
@@ -117,8 +133,14 @@ async function sessionEvent(directory: string, type: string, sessionID: string,
   }
 }

but the message says that I am going to send it to chainloop controlplane, not local

image

this is my chainloop.yaml


  Press Ctrl-C again to exit                                                                                                                                                      ◐ medium · /effort
maximus ➜  chainloop git:(main) ✗ chainloop wf ls
ERR authentication required, please run "chainloop auth login"
maximus ➜  chainloop git:(main) ✗ cat .chainloop.yml
# organization and projectName are used for Chainloop Trace
# https://docs.chainloop.dev/reference/operator/trace
organization: chainloop
projectName: chainloop
# This indicates the [current version]+next
# to indicate that we are building a new version of the project
projectVersion: v1.123.0+next
# Experimental feature used by Chainloop labs shared workflow https://github.com/chainloop-dev/labs
# It maps the material names with location in disk so they get automatically attested
scorecards:
  - name: sarif-results
    path: metadata/results.sarif
traceStorage: LOCAL

@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.

@matiasinsaurralde ptal at my comments, I can't make it work as well as I added some additional feedback

@jiparis jiparis self-assigned this Oct 15, 2026
@jiparis

jiparis commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

I'd also like to discuss the right default location, would it be

(default: under .git/chainloop-trace/local-sessions);

or should it be .chainloop/... since that's what we do now with stored specs, etc?

I think .git is fine, since it's hidden and won't be committed. The reason specs are stored in .chainloop is because the agent might not be allowed to write outside the session working folder, and .git is outside when working in worktrees.

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
@jiparis jiparis changed the title feat(trace): local storage mode for AI coding sessions feat(trace): online and offline trace modes for AI coding sessions Oct 15, 2026

@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 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

Comment thread app/cli/internal/trace/config/config.go Outdated
Comment thread app/cli/pkg/action/trace_hook_handler.go Outdated
Comment thread app/cli/cmd/trace_hook.go Outdated
Comment thread app/cli/documentation/cli-reference.md Outdated
Comment thread app/cli/documentation/cli-reference.md Outdated
Comment thread app/cli/internal/trace/README.md Outdated
Comment thread app/cli/cmd/trace_init.go Outdated
Comment thread app/cli/pkg/action/trace_agent_hook.go Outdated
@jiparis jiparis changed the title feat(trace): online and offline trace modes for AI coding sessions feat(trace): local storage mode for AI coding sessions Oct 15, 2026
…ther"

This reverts commit db6b419.

Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>

Assisted-by: Claude Code

@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 23 files (changes from recent commits).

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

View guided diff | Re-trigger cubic

Comment thread app/cli/pkg/action/trace_local_store.go
Comment thread app/cli/cmd/trace_init.go Outdated
Comment thread app/cli/cmd/trace_init.go Outdated
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

@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 11 files (changes from recent commits).

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

View guided diff | Re-trigger cubic

Comment thread app/cli/cmd/trace_run.go Outdated
Comment thread app/cli/documentation/cli-reference.md Outdated
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
@jiparis

jiparis commented Oct 15, 2026

Copy link
Copy Markdown
Member

I took over this PR to address the review. These are the changes since 95c1353:

trace init runs only once per repository (87da772)

  • The next steps after trace init --local no longer tell each teammate to run trace init --local after cloning.
  • In a clone, the first traced session installs the git hooks, as in control-plane mode. A new test covers a local-mode clone with no projectName.

Local mode tip for authentication errors (69e74fc)

  • When a control-plane trace init fails because the user is not authenticated, the error has a tip:
    ERR authentication required, please run "chainloop auth login"
    Tip: run "chainloop trace init --local" to configure it in local mode
    
  • The tip shows only in trace init. Other commands keep the usual message.

tracePath is the trace directory in every mode (de543f3)

  • --trace-path no longer turns on --local.
  • tracePath in .chainloop.yml sets the directory where Chainloop Trace keeps its data, in local and control-plane mode. The default stays .git/chainloop-trace. A relative path starts at the repository (or worktree) root.
  • Local mode writes the sessions to the local-sessions folder of that directory.
  • All git stores of a repository come from state.NewRepoStore, so the git hooks, the agent hooks, the pre-push hook, trace run and trace uninstall use the same directory. Linked worktrees keep separate state, as before.

--local for trace run (2ac1bd6)

  • trace run takes its settings from its flags. Local mode is the --local flag, not traceStorage in .chainloop.yml.
  • With --local, the control-plane flags (--org, --project, --workflow, --version, --contract) are rejected.
  • The run writes a run-local marker 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.

cubic findings (33bda24)

  • A local store that skips a session now returns an error. The pre-push hook then warns, or blocks the push when traceRequired is set. The skipped session stays for the next push.
  • The trace init and trace run help say that the workflow, the login and the identity flags apply to control-plane mode only.

Text (020af50)

  • The help, flag descriptions, init output, errors, logs and session-start banner of local mode follow Simplified Technical English (ASD-STE100).
  • The next steps no longer say "the push tells you where".

Open item

  • cubic finding 4282238976: in a repository where trace init already ran, trace run can select sessions of earlier AI commits instead of the session of the run. This logic in gatherPushSessions existed before this PR, and control-plane trace run has the same gap.

AI disclosure: these changes were written with Claude Code.

@migmartri

Copy link
Copy Markdown
Member

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
@jiparis

jiparis commented Oct 15, 2026

Copy link
Copy Markdown
Member

Did you make any changes in teh manifest?

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:

{
  "generated_at": "2026-10-15T14:56:04Z",
  "signed": false,
  "is_attestation": false,
  "redaction": true,
  "evidence_record": "ai-coding-session-495f5e",
  "materials": [
    {
      "name": "ai-coding-session-495f5e",
      "kind": "CHAINLOOP_AI_CODING_SESSION",
      "digest": "sha256:598f072939f2050435dfeb8df2073cd81577990debde81ff64d111dcab3f405f",
      "file": "materials/sha256-598f072939f2050435dfeb8df2073cd81577990debde81ff64d111dcab3f405f",
      "annotations": {
        "chainloop.material.aiagent.model": "claude-opus-5-5",
        "chainloop.material.aiagent.name": "claude-code",
        "chainloop.material.aisession.mode": "coding",
        "chainloop.material.redacted": "true",
        "chainloop.material.redaction.count": "0",
        "chainloop.material.redaction.ruleset": "3",
        "chainloop.material.size": "863541"
      }
    }
  ]
}

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
@jiparis jiparis changed the title feat(trace): local storage mode for AI coding sessions feat(trace): local storage mode for AI coding sessions Oct 15, 2026
@jiparis

jiparis commented Oct 15, 2026

Copy link
Copy Markdown
Member

New changes since the last summary:

statement.json replaces manifest.json (c1e041b)

  • A stored session now has statement.json as its index. It is the in-toto statement that a push would sign, with the chainloop.dev/attestation/v0.2 predicate. The Chainloop renderer makes it from the same kind of crafting state as a push.
  • It has no envelope and no signature, so it is not an attestation.
  • The statement lists each material by name, type, and digest. The content stays in materials/<digest>, not inline. Materials are not marked as uploaded to a CAS, because the content is on local disk.
  • The subject names the ai-coding-session workflow and the git head commit.
  • The new crafter.GitRepoHead helper reads the head commit with no platform verification, so local mode makes no network call.
  • To answer the question about the manifest: yes, the custom manifest.json is gone. Readers and policies that read a Chainloop statement can read a stored session. A later change can sign a stored statement and push it to the control plane.

--local for trace run (2ac1bd6)

  • trace run takes local mode from the --local flag and does not read traceStorage. With --local, the control-plane flags are an error.

Spec (5cba713)

  • Decision Record rows D-021 to D-024 record the changes from this review. They cover the statement as the index, tracePath as the trace directory, the --local flag of trace run, and no trace init for a clone.

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
@migmartri

Copy link
Copy Markdown
Member

@jiparis let me know once this is ready for the final test?

@jiparis

jiparis commented Oct 15, 2026

Copy link
Copy Markdown
Member

@jiparis let me know once this is ready for the final test?

you can go ahead

@jiparis

jiparis commented Oct 15, 2026

Copy link
Copy Markdown
Member

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"
  }
}

@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.

nitpick, maybe instad of materials, call it evidence

Image

migmartri
migmartri previously approved these changes Oct 15, 2026

@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.

Awesome

Image

@jiparis

jiparis commented Oct 15, 2026

Copy link
Copy Markdown
Member

nitpick, maybe instad of materials, call it evidence

Image

makes sense. I'll request a new review after the change.

…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
@migmartri
migmartri merged commit 580a801 into main Oct 15, 2026
17 checks passed
@migmartri
migmartri deleted the matias/issue-3582-trace-local-mode branch October 15, 2026 17:53
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.

4 participants