feat(trace): local storage mode for AI coding sessions
Refs PFM-7675
Summary
Chainloop Trace records an AI coding session and attests it to a control plane on git push. This adds a local storage mode: a repository records its sessions and stores the assembled evidence set on disk, with no control plane, login, or network.
The mode is a committed repository policy in .chainloop.yml:
traceStorage-CONTROL_PLANE(default) orLOCALtracePath- optional location for stored sessions (defaults under the per-clone trace state)traceRequired- replaces the deprecatedrequireTrace
chainloop trace init --local sets a repository up with no control plane (R-008). In local mode the pre-push hook 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 (R-002, R-003, R-006, R-009). trace run honors the mode as well. Secret redaction stays on by default, with a CHAINLOOP_TRACE_NO_REDACT opt-out for a trusted local store (R-004). No control-plane connection is opened in local mode (R-005), and stored sessions are preserved across a trace uninstall.
Implements the design in docs/specs/issue-3582-trace-evidence-export.md.
Refs #3582
Flow
- Run
trace init --local:
% chainloop trace init --local
┃ Harnesses to trace in this repository
┃ > ✓ claude-code
┃ • cursor
┃ • opencode
Congratulations, your repository is initialized in local mode
storage local (no control plane)
sessions stored under .git/chainloop-trace/local-sessions on push
harnesses claude-code
What's next
1. Commit these files so every clone records locally:
git add .chainloop.yml .claude/settings.json
2. Start claude-code and write some code
3. Commit and push as usual. The session evidence is written to disk, with no
control plane; the push tells you where
Learn more: https://docs.chainloop.dev/ai-sessions
- Run Claude:
% claude
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Accessing workspace:
/private/var/folders/f2/x8m881b97f7f3b2c_tgd8ry00000gn/T/tmp.kic78wD1Uu
Quick safety check: Is this a project you created or one you trust? (Like your own code, a well-known open source project, or work from your team). If not, take a moment to review what's in this folder first.
Claude Code'll be able to read, edit, and execute files here.
Security guide
❯ No, exit
Yes, I trust this folder
Enter to confirm · Esc to cancel
% claude
▗ ▗ ▖ ▖ Claude Code v2.1.296
Opus 4.8 with xhigh effort · Claude Team
▘▘ ▝▝ /private/var/folders/f2/x8m881b97f7f3b2c_tgd8ry00000gn/T/tmp.kic78wD1Uu
⚠ Your login expires in 2 days · run /login to renew
Introducing Sonnet 5.5
Sonnet 5.5 is here, and it's faster and excels at well-scoped tasks. Opus 5.5 is still your default. Switch with /model.
⎿ SessionStart:startup says:
Chainloop Trace is recording this session in local mode.
Its evidence will be stored on disk at /var/folders/f2/x8m881b97f7f3b2c_tgd8ry00000gn/T/tmp.kic78wD1Uu/.git/chainloop-trace/local-sessions when you push.
Opus 4.8 with xhigh effort · Claude Team
▘▘ ▝▝ /private/var/folders/f2/x8m881b97f7f3b2c_tgd8ry00000gn/T/tmp.kic78wD1Uu
⚠ Your login expires in 2 days · run /login to renew
Introducing Sonnet 5.5
Sonnet 5.5 is here, and it's faster and excels at well-scoped tasks. Opus 5.5 is still your default. Switch with /model.
⎿ SessionStart:startup says:
Chainloop Trace is recording this session in local mode.
Its evidence will be stored on disk at /var/folders/f2/x8m881b97f7f3b2c_tgd8ry00000gn/T/tmp.kic78wD1Uu/.git/chainloop-trace/local-sessions when you push.
❯ create a hello world in go as main.go
⏺ I'll create the Go hello world file. (This is a one-line request with no external spec or source to capture, so nothing goes into the spec folder.)
⏺ Write(main.go)
⎿ Wrote 7 lines to main.go
1 package main
2
3 import "fmt"
4
5 func main() {
6 fmt.Println("Hello, World!")
7 }
⏺ Created main.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Run it with go run main.go.
✻ Worked for 8s · done 3:12 PM
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ /exit
- Create a local remote and push:
% git add main.go
% git commit -s -m 'Add main'
INF commit record saved sha=2771434712b37daa0bb8a2e5b6334914f6cfaaf3
[main (root-commit) 2771434] Add main
1 file changed, 7 insertions(+)
create mode 100644 main.go
% git push -u origin main
INF AI coding session evidence stored at /var/folders/f2/x8m881b97f7f3b2c_tgd8ry00000gn/T/tmp.kic78wD1Uu/.git/chainloop-trace/local-sessions sessions=1
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 10 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 347 bytes | 347.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To ../remote.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
- Verify the recorded local sessions:
% find .git/chainloop-trace/local-sessions
.git/chainloop-trace/local-sessions
.git/chainloop-trace/local-sessions/b8c89e2c-9cda-47e2-a326-38d68c9ae0ef
.git/chainloop-trace/local-sessions/b8c89e2c-9cda-47e2-a326-38d68c9ae0ef/materials
.git/chainloop-trace/local-sessions/b8c89e2c-9cda-47e2-a326-38d68c9ae0ef/materials/sha256-0840bca458fa3e72dfe87fdd43b3b731aa88fecaf3cfe33138231b4c5d592d67
.git/chainloop-trace/local-sessions/b8c89e2c-9cda-47e2-a326-38d68c9ae0ef/manifest.json