aflock replay docs

How it works, how to build it, where the code lives, and how to extend it.

Contents

What is it?

aflock replay lets you take a recorded Claude Code session (.jsonl) and an aflock policy (.aflock), and see exactly what would happen if that policy were enforced — which tool calls would be ALLOWed, DENYed, or flagged as ASK (require approval).

It uses the real aflock policy evaluator compiled to WebAssembly. The same Go code that runs in the CLI runs in your browser. No server, no approximations — exact parity.

Cole's original idea: "I like a tool where you can drag in a Claude session and validate it against an AfLock policy. If it works we could create a replay simulator — which would allow us to create a verifier for aflock, would allow us to code on it much faster with agents. It will also make a great demo."

Architecture

Browser index.html ↓ loads wasm_exec.js (Go WASM runtime) ↓ loads replay.wasm (aflock evaluator compiled to WebAssembly) ↓ registers window.aflockReplay() ← JavaScript calls this ↓ internally uses internal/replay.Run() internal/policy.NewEvaluator() policy.EvaluatePreToolUse() ↓ returns JSON report (same schema as aflock replay --format json) No server involved. Static files served by any HTTP server.

Key insight

The WASM binary is built from aflock/cmd/wasm/main.go which lives inside the aflock module. This is the only way to access Go's internal/ packages — code must be in the same module. The binary imports:

How WASM Works

What is WebAssembly?

WebAssembly (WASM) is a binary format that runs in browsers at near-native speed. Go can compile to WASM using GOOS=js GOARCH=wasm. The result is a .wasm file that browsers execute via the WebAssembly API.

Go WASM specifics

Go WASM uses syscall/js to bridge Go and JavaScript. The Go code registers functions on js.Global() that JavaScript can call directly:

// Go side (cmd/wasm/main.go)
js.Global().Set("aflockReplay", js.FuncOf(aflockReplay))

// JavaScript side (app.js)
const report = JSON.parse(window.aflockReplay(sessionText, policyText));

The WASM lifecycle

  1. Browser loads wasm_exec.js (Go's JS runtime support)
  2. Browser fetches and instantiates replay.wasm
  3. go.run(instance) starts the Go runtime inside the browser
  4. Go's main() registers global functions and blocks with select{}
  5. JavaScript calls window.aflockReplay() which runs Go code synchronously
  6. Go parses the session, evaluates each action, returns a JSON report

Binary size

The .wasm file is ~34 MB (includes the Go runtime, garbage collector, etc.). With gzip compression (automatic in nginx/Cloudflare), it's ~8 MB over the wire. First load takes 2-3 seconds, then it's cached.

How Evaluation Works

When you click "Start Replay", the JavaScript calls window.aflockReplay(sessionJSONL, policyJSON) which runs the real aflock evaluator. Here's what happens inside:

1. Parse the session

The JSONL file is scanned line by line. Each assistant message with type: "tool_use" content blocks is extracted as an action with: tool name, tool ID, input parameters, and raw JSON input.

2. Infer project root

Sessions contain absolute file paths like /Users/you/project/src/app.js, but policies use relative globs like src/**. The WASM computes the longest common directory prefix across all file paths to determine the project root, then relativizes paths before matching.

3. Evaluate each action

For each action, policy.EvaluatePreToolUse() checks in order:

  1. Tool deny — Is the tool in tools.deny?
  2. Tool allow — If tools.allow is set, is the tool in it?
  3. File rules — For Read/Write/Edit/Glob/Grep: check files.deny, files.allow, files.readOnly
  4. Domain rules — For WebFetch: check domains.deny, domains.allow
  5. Bash analysis — For Bash: extract commands, check bypass patterns, check requireApproval
  6. Grants — Check secret/API/storage patterns
  7. Default — Allow

4. Generate report

Results are collected into a JSON report matching the aflock replay --format json schema. The UI renders this report step by step.

Building

Prerequisites

Build the WASM binary

# From the aflock-replay directory
make wasm

# Or manually:
cd /path/to/aflock
GOOS=js GOARCH=wasm go build -o /path/to/aflock-replay/web/static/replay.wasm ./cmd/wasm/

Serve the UI

# Simple Python server (no Go needed)
make dev
# → http://localhost:8080

# Or use any static server:
cd web && npx serve .
cd web && caddy file-server --listen :8080

Rebuild when aflock changes

# Pull latest aflock
cd /path/to/aflock && git pull

# Rebuild WASM
cd /path/to/aflock-replay && make wasm

# That's it — the UI now uses the updated evaluator

Project Structure

aflock-replay/
├── web/                           # WASM version (primary)
│   ├── index.html                 # Static landing page + replay UI
│   ├── docs/                      # This documentation
│   │   └── index.html
│   └── static/
│       ├── app.js                 # Frontend JavaScript (UI logic)
│       ├── replay.wasm            # Go evaluator → WASM (34 MB)
│       └── wasm_exec.js           # Go WASM runtime (from GOROOT)
├── standalone/                    # JS-only fallback (no WASM)
│   └── index.html                 # Self-contained, JS policy evaluator
├── cmd/server/main.go             # Optional Go HTTP server
├── Makefile
├── go.mod
└── README.md

aflock/cmd/wasm/main.go            # WASM build target (in aflock repo fork)
FilePurposeWhen it changes
cmd/wasm/main.goGo WASM entry point, registers JS functionsWhen adding new WASM API functions
web/static/replay.wasmCompiled WASM binaryRebuild when aflock evaluator changes
web/static/app.jsUI: drop zones, playback, renderingWhen changing the UI
web/index.htmlHTML structure + CSSWhen changing layout/styles
web/static/wasm_exec.jsGo WASM JS runtimeWhen upgrading Go version
standalone/index.htmlJS-only version (no WASM)When updating JS evaluator

WASM API Reference

The WASM binary registers these functions on window:

aflockReplay(sessionJSONL, policyJSON) → string

The main function. Takes raw session JSONL content and raw policy JSON, returns a JSON report string.

const report = JSON.parse(window.aflockReplay(sessionText, policyText));
// report.verdict === "pass" | "fail"
// report.actions === [{index, tool, id, input, decision, reason}]
// report.allowCount, report.denyCount, report.askCount

aflockParseSession(sessionJSONL) → string

Parses session metadata without evaluating against a policy.

const info = JSON.parse(window.aflockParseSession(sessionText));
// info.model, info.turns, info.tokensIn, info.tokensOut, info.toolCalls, info.actions

aflockEvaluateAction(toolName, toolInputJSON, policyJSON, projectRoot) → string

Evaluates a single action against a policy.

const result = JSON.parse(window.aflockEvaluateAction(
  "Read",
  '{"file_path":"src/app.js"}',
  policyText,
  "/Users/you/project"
));
// result.decision === "allow" | "deny" | "ask"
// result.reason === "..." (empty if allow)

aflockVersion() → string

Returns the WASM module version.

window.aflockVersion() // "aflock-wasm v0.1.0"

Report JSON Schema

Matches aflock replay --format json exactly:

{
  "policy": "policy-name",
  "policyPath": "browser",
  "model": "claude-opus-4-6",
  "turns": 15,
  "toolCalls": 5,
  "tokensIn": 42000,
  "tokensOut": 1091,
  "allowCount": 3,
  "denyCount": 1,
  "askCount": 1,
  "verdict": "fail",
  "actions": [
    {
      "index": 1,
      "tool": "Read",
      "id": "toolu_01abc",
      "input": {"file_path": "src/app.js"},
      "decision": "allow",
      "reason": ""
    }
  ]
}

Session Format

Claude Code stores sessions at ~/.claude/projects/<project-slug>/<session-uuid>.jsonl.

Each line is a JSON object with a message field:

{"type":"msg","message":{"role":"user","content":"Read src/main.go"}}
{"type":"msg","message":{"role":"assistant","model":"claude-opus-4-6","content":[
  {"type":"tool_use","name":"Read","id":"toolu_01abc","input":{"file_path":"src/main.go"}}
],"usage":{"input_tokens":3200,"output_tokens":45}}}

The replay extracts:

FieldLocationExample
Tool namecontent[].nameRead, Bash, Edit
Tool inputcontent[].input{"file_path":"src/main.go"}
Modelmessage.modelclaude-opus-4-6
Tokensmessage.usagein: 3200, out: 45
TurnsCount of role: "user"15

Finding sessions

# List recent sessions
ls -lt ~/.claude/projects/*/*.jsonl | head -10

# Find sessions for a specific project
ls ~/.claude/projects/-Users-you-my-project/*.jsonl

Policy Format

An .aflock file is a JSON document describing constraints. Key sections:

{
  "version": "1.0",
  "name": "my-policy",
  "identity": { "allowedModels": ["claude-opus-*"] },
  "tools": {
    "allow": ["Read", "Edit", "Bash"],
    "deny": ["Task", "Agent"],
    "requireApproval": ["Bash:rm *", "Bash:git push*"]
  },
  "files": {
    "allow": ["src/**", "tests/**"],
    "deny": ["**/.env", "**/secrets/**"],
    "readOnly": ["package.json", "go.mod"]
  },
  "domains": {
    "allow": ["github.com", "pkg.go.dev"],
    "deny": ["*.evil.com"]
  },
  "limits": {
    "maxTurns": {"value": 30, "enforcement": "post-hoc"},
    "maxSpendUSD": {"value": 5.00, "enforcement": "fail-fast"}
  }
}

See the full policy specification for all fields.

Deployment Modes

ModeCommandEvaluatorServer Needed?Accuracy
WASMmake devReal Go code in browserAny static serverExact
Go Servermake go-serverCalls aflock replay CLIGo server + aflock binaryExact
Standalone JSmake standaloneJS port of evaluatorAny static serverApproximate

Recommended: WASM mode. Same evaluator as the CLI, runs 100% client-side, no server-side processing needed.

Troubleshooting

WASM fails to load

All actions show DENY

34 MB WASM file

readOnly files showing DENY for Read