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

MA
1h ago
+2042-301

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) or LOCAL
  • tracePath - optional location for stored sessions (defaults under the per-clone trace state)
  • traceRequired - replaces the deprecated requireTrace

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

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

  1. 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'.
  1. 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