How it works, how to build it, where the code lives, and how to extend 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."
aflock replay --format json)
No server involved. Static files served by any HTTP server.
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:
internal/replay — session parsing, report generationinternal/policy — policy evaluator (tool/file/domain/bash rules)pkg/aflock — policy typesWebAssembly (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 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));
wasm_exec.js (Go's JS runtime support)replay.wasmgo.run(instance) starts the Go runtime inside the browsermain() registers global functions and blocks with select{}window.aflockReplay() which runs Go code synchronouslyThe .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.
When you click "Start Replay", the JavaScript calls window.aflockReplay(sessionJSONL, policyJSON) which runs the real aflock evaluator. Here's what happens inside:
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.
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.
For each action, policy.EvaluatePreToolUse() checks in order:
tools.deny?tools.allow is set, is the tool in it?files.deny, files.allow, files.readOnlydomains.deny, domains.allowrequireApprovalResults are collected into a JSON report matching the aflock replay --format json schema. The UI renders this report step by step.
cmd/wasm/main.go (your fork or the wasm branch)# 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/
# 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
# 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
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)
| File | Purpose | When it changes |
|---|---|---|
cmd/wasm/main.go | Go WASM entry point, registers JS functions | When adding new WASM API functions |
web/static/replay.wasm | Compiled WASM binary | Rebuild when aflock evaluator changes |
web/static/app.js | UI: drop zones, playback, rendering | When changing the UI |
web/index.html | HTML structure + CSS | When changing layout/styles |
web/static/wasm_exec.js | Go WASM JS runtime | When upgrading Go version |
standalone/index.html | JS-only version (no WASM) | When updating JS evaluator |
The WASM binary registers these functions on window:
aflockReplay(sessionJSONL, policyJSON) → stringThe 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) → stringParses 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) → stringEvaluates 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() → stringReturns the WASM module version.
window.aflockVersion() // "aflock-wasm v0.1.0"
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": ""
}
]
}
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:
| Field | Location | Example |
|---|---|---|
| Tool name | content[].name | Read, Bash, Edit |
| Tool input | content[].input | {"file_path":"src/main.go"} |
| Model | message.model | claude-opus-4-6 |
| Tokens | message.usage | in: 3200, out: 45 |
| Turns | Count of role: "user" | 15 |
# List recent sessions
ls -lt ~/.claude/projects/*/*.jsonl | head -10
# Find sessions for a specific project
ls ~/.claude/projects/-Users-you-my-project/*.jsonl
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.
| Mode | Command | Evaluator | Server Needed? | Accuracy |
|---|---|---|---|---|
| WASM | make dev | Real Go code in browser | Any static server | Exact |
| Go Server | make go-server | Calls aflock replay CLI | Go server + aflock binary | Exact |
| Standalone JS | make standalone | JS port of evaluator | Any static server | Approximate |
Recommended: WASM mode. Same evaluator as the CLI, runs 100% client-side, no server-side processing needed.
file:// (browsers block WASM loading from local files)replay.wasm exists in web/static/: run make wasmaflock replay --session file.jsonl --policy .aflock — if the CLI shows ALLOW, rebuild the WASM (make wasm).readOnly files showing DENY for ReadreadOnly files are implicitly allowed for reads, only denied for writes.make wasm