Documentation Index
Fetch the complete documentation index at: https://quintsecurity.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
v1.0.3 — Session Attribution & Edge Stability
Released April 25, 2026. Three edge-side bugs closed and one API added. The local pipeline is now production-shape: every captured byte is attributable to a specific AI agent invocation, and the macOS extensions survive the load patterns that were killing them.What’s New
Every audit row is now session-attributable
Before this release,audit_log rows carried only a per-tunnel trace_id. Two simultaneous Claude Code sessions interleaved in the log with no way to group by invocation. Now every row stamps session_id (the unisession ID — {rootPID}-{startUnixMs}) and process_pid at capture time, pulled from unisession.Tracker.SessionByPID.
What this enables:
- Two terminals running
claudein parallel → two distinctsession_idvalues, cleanly separated - Cloud
QuintEventpayloads already carrysession_id— local audit and cloudactionstable now share the same key - New
audit.QueryOpts.SessionID/ProcessPIDfilters expose this via/api/audit?session_id=...
NULL for these fields.
Decoded Timeline API
New endpoint:GET /api/sessions/timeline?pid=N or ?session_id=X.
Server-side decoder walks the full wire format:
request, assistant_text, and tool_call events — no base64 soup, no binary framing, fully reconstructed tool_input JSON for each tool use.
Historical sessions visible after the process exits
unisession.Tracker reaps sessions ~10 seconds after their root PID exits. Before this release, that reaping erased captured session data from the viewer even though it remained in audit_log. New helper audit.DB.HistoricalSessions(limit) groups audit rows by (session_id, process_pid) so ended sessions remain discoverable. /api/es/sessions merges live tracker state with historical audit data, ordered by MAX(timestamp) descending.
Fixed
SSE responses no longer hang Claude Code
A prior fix attempted to re-chunk SSE responses on egress but was looking at the wrong place for the upstreamTransfer-Encoding. http.ReadResponse strips Transfer-Encoding: chunked from the header map and stashes it in resp.TransferEncoding. The fix always re-emits SSE and AWS eventstream responses as chunked regardless of upstream framing — the only way a streaming response on a keep-alive connection can signal end-of-stream to the client.
Before: Claude Code completed the visible response, then hung on the spinner forever. Every subsequent turn on the same keep-alive connection stalled.
After: Responses terminate cleanly. /debug/flows shows 0 stuck flows across multi-turn sessions.
NE extension stops burning 99% CPU
macOS’scpu_resource watchdog was SIGKILLing the NetworkExtension every ~90 seconds under load. Diagnostic report showed all 35 stack samples identical: emitFlowEvent → sendRaw → write (libsystem_kernel).
Root cause: NE’s socket to the daemon is O_NONBLOCK. When the daemon was slow or its read loop stalled, write() returned -1 with errno=EAGAIN, and the socket client’s if errno == EAGAIN { continue } pegged a CPU core. macOS’s watchdog kicked in after 50% average over 180 seconds.
Fix: replaced the hot loop with a per-frame 50 ms poll budget. On EAGAIN, call poll(POLLOUT) with the remaining budget. If poll() returns 0 (timeout), drop the frame and reconnect the socket. Telemetry is lossy-at-the-edge by design — dropping an event is preferable to killing the extension.
Before: NE process died every 90 seconds under load. Required bouncing the QuintAgent app to recover.
After: NE survives arbitrary daemon backpressure. Verified with strings on the installed binary:
Tool result capture in audit DB raised from 1 KB to 10 KB
The audit write path was truncating tool results at 1 KB, which cut off most Bash outputs mid-line. Raised to 10 KB. The LLM parser upstream already caps at 10 KB, so this just aligns the two.Tool results no longer dropped on the pairing turn
tool_use events are echoed on every subsequent turn of an Anthropic conversation, but the corresponding tool_result lands in the next request’s message array — produced locally by the client after the tool ran. The prior toolDedup.Seen(id) check dropped that second sighting, and with it the tool_result payload.
Replaced with toolDedup.Observe(id, hasResult) returning one of three states:
FirstSeen— brand-newtool_use, full insert path (scoring, audit row, pipeline event)ResultAdded— sametool_useseen again, but now carrying thetool_result. Takes the backfill path:UPDATE audit_log SET response_json = ? WHERE tool_use_id = ? AND response_json IS NULL, plus a supplement pipeline event taggedsource: "tool_result"withtool_use_idin labels. No re-scoring, no duplicate forwarding.Duplicate— nothing new, dropped.
QUINT_SENTINEL_abc123 captured verbatim, /etc/hosts contents captured, env dumps captured in the live viewer timeline.
Risk scores now populated on daemon audit writes
Gateway mode has been writingrisk_score / risk_level on tools/call audit rows since v1.0.0, but the daemon (callbacks.go::onToolCall) was not — every daemon-captured row had NULL scoring fields. Fixed by threading tcResult.RiskScore (value, level, base score, scoring source) into the audit.Entry at insert time, mirroring the gateway pattern. The local viewer now renders risk badges (low/medium/high/critical) inline on every tool call event.
Sub-Agent Detection: In-Process Task Dispatch
New detector inforwardproxy.SubagentDetector that identifies when Claude Code’s in-process Task / Agent tool spawns a child session without a new CONNECT tunnel or process. Existing passive detection covers cross-process spawns; this handles the case where the child runs inside the parent’s tunnel.
Mechanism:
- On every POST with a JSON body, hash the top-level
systemprompt with FNV-1a - Track
(tunnelKey → {parentHash, currentHash, msgCount})state - Gate
subagent_start: hash changed ANDmessages.length <= 2(fresh conversation inside the same tunnel) - Gate
parent_resume: hash reverted toparentHashANDmessages.length > 2(parent turn appended to prior history)
{parentSession}:sub:{counter}, threaded through AgentToolEvent.ParentSessionID on both request-parse and response-parse emission sites, and written to audit_log.parent_session_id. The viewer renders nested rows with a purple left border and sub badge under their parent session.
Edge cases handled:
- Mid-session prompt rewrites (MCP servers connecting / CLAUDE.md edits change the hash but keep
msgCount > 2) — not classified as subagent - Multiple sequential subagents in the same parent — counter increments (
:sub:1,:sub:2,:sub:3) - Separate CONNECT tunnels keep independent detector state
subagent_test.go. Verified live: :sub:2 and :sub:3 minted correctly during a real Claude Code session with two Task dispatches.
Developer Experience
Local viewer drill-down
http://localhost:8080/viewer?token=<dashboard-token> gained session drill-downs. Click a session card → inline timeline with tool calls, assistant text, and request metadata. Cards sort by most-recent activity (live + ended together). Auto-refresh preserves expanded drill-downs and scroll position. Keyboard shortcuts: / focuses search, Esc collapses all. “Copy JSON” button per timeline dumps the decoded events for debugging.
This is an internal admin surface — not part of the customer-facing dashboard, but useful for validating that the local pipeline is capturing what it should.
Migration Notes
- Additive-only schema changes; no code or config changes required by callers
audit.LogOptsgainedSessionID stringandProcessPID intfields — existing call sites that don’t populate them continue to work (stored asNULL)forwardproxy.OptionsgainedSessionLookup func(pid int) string— optional;nilleaves rows un-attributed- Cloud
QuintEventschema unchanged —session_idfield was already present; callers just populate it more consistently now
What’s Next
- NE auto-reconnect on daemon restart (today: NE survives backpressure but can still detach cleanly on a daemon crash — reconnect is lazy on next send)
- End-to-end integration test harness covering the two interception paths + session attribution + cloud delivery
- Code-signing path cleanup for extension builds (currently
xcodebuild -configuration Releaseis the only supported install path;swift build+ ad-hoc codesign produces a duplicate-TeamID bundle that macOS treats as a separate extension)