Ever wondered what your AI coding agent is actually sending over the network? As part of our work on AgentSight — an eBPF-powered observability tool that monitors AI agents at system boundaries without modifying their code — we needed to intercept Claude Code's encrypted API traffic to understand its communication patterns. AgentSight can capture both what an AI agent intends to do (by intercepting LLM API traffic) and what it actually does (by monitoring kernel events), then correlate the two into a complete causal chain. To make this work, we first need to crack open the agent's encrypted network traffic. Sounds straightforward, right? Just hook into Node.js's OpenSSL with sslsniff, like we did with older versions of Claude Code. Back then, Claude Code ran on Node.js with dynamically linked libssl.so, and standard eBPF-based SSL hooking worked out of the box.
But everything changed. The new Claude Code is a 213 MB stripped Bun binary with BoringSSL statically linked and all symbols removed. No libssl.so, no SSL_write symbol to hook — standard tools are completely blind to it. So we did what any self-respecting systems researcher would do: we asked Claude Code to reverse engineer itself. Yes, the very AI agent we were trying to monitor became our reverse engineering assistant, analyzing its own binary, searching for byte patterns, and helping us locate the stripped SSL functions inside itself.
This post chronicles that complete journey: from discovering why standard tools fail, to matching byte patterns against Bun's open-source profile builds, to finally capturing every single API call — including the full /v1/messages conversation stream with prompts, responses, and SSE events. Along the way, we chased a fascinating red herring about Bun's "dual TLS paths" that turned out to be a simple timing issue. The techniques here apply to any stripped binary with statically-linked SSL, not just Claude Code.
AgentSight is open source at https://github.com/agent-sight/agentsight, and the arxiv paper describing the full system is available at https://arxiv.org/abs/2508.02736.
Executive Summary
Claude Code CLI is a Bun v1.3.9-canary application with BoringSSL
statically linked and symbols stripped. We successfully modified sslsniff to
auto-detect BoringSSL functions via byte-pattern matching and can now capture
ALL SSL/TLS traffic including the conversation API (/v1/messages).
What works
| Traffic Type | Endpoint | HTTP Client | Captured? |
|---|---|---|---|
| Heartbeat | GET /api/hello | axios/1.8.4 | YES |
| Telemetry | POST /api/event_logging/batch | axios/1.8.4 | YES |
| Datadog logs | POST /api/v2/logs | axios/1.8.4 | YES |
| Conversation API | POST /v1/messages?beta=true | Bun native fetch | YES |
All traffic flows through a single HTTP Client thread via BoringSSL
SSL_write/SSL_read. The initial hypothesis that Bun's native fetch()
used a separate TLS path was incorrect — it uses the same BoringSSL functions
through uSockets. Earlier test captures missed /v1/messages due to timing
(no new messages were submitted during the capture window).
Captured data includes
- Request: Full HTTP headers (Authorization, anthropic-beta, etc.) and JSON body (model, messages array, system prompt)
- Response: Complete SSE streaming events (message_start, content_block_delta with text/tool_use, message_stop)
- Protocol: HTTP/1.1 over TLS (not HTTP/2)
Phase 1: Initial Reconnaissance
Goal
Determine if the existing sslsniff can monitor Claude out of the box.
Commands & Results
# Check if claude is running $ ps aux | grep claude yunwei37 847282 14.8 0.6 75318520 838308 pts/7 Rl+ claude # Try standard sslsniff with comm filter $ sudo timeout 10 ./bpf/sslsniff -c claude # Result: NO OUTPUT — nothing captured in 10 seconds # Check sslsniff help for relevant options $ sudo ./bpf/sslsniff -h # Lists: -p PID, -c COMMAND, --binary-path PATH, etc. Result: Standard sslsniff captures nothing from claude processes.
Root Cause Investigation
# Check claude binary type $ file ~/.local/share/claude/versions/2.1.39 ELF 64-bit LSB executable, x86-64, dynamically linked # Check dynamic library dependencies $ ldd ~/.local/share/claude/versions/2.1.39 libc.so.6 libpthread.so.0 libdl.so.2 libm.so.6 libstdc++.so.6.0.33 libgcc_s.so.1 # NOTE: NO libssl.so or libcrypto.so! # Check loaded libraries at runtime $ sudo cat /proc/847282/maps | grep "\.so" | awk '{print $NF}' | sort -u /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/libc.so.6 /usr/lib/x86_64-linux-gnu/libdl.so.2 /usr/lib/x86_64-linux-gnu/libgcc_s.so.1 /usr/lib/x86_64-linux-gnu/libm.so.6 /usr/lib/x86_64-linux-gnu/libnss_mdns4_minimal.so.2 /usr/lib/x86_64-linux-gnu/libpthread.so.0 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33 # CONFIRMED: no SSL library loaded at runtime either Conclusion: Claude uses a statically-linked SSL library embedded in the binary.
Phase 2: Understanding Why Standard Hooking Fails
Attempt: Use --binary-path to hook SSL in the Claude binary directly
$ sudo ./bpf/sslsniff -c claude \ --binary-path ~/.local/share/claude/versions/2.1.39 # ERROR: # libbpf: elf: 'SSL_write' is 0 in symtab for '...2.1.39': # try using shared library path instead # no program attached for probe_SSL_rw_enter: No such file or directory Why it fails: sslsniff uses bpf_program__attach_uprobe_opts with
.func_name = "SSL_write", which searches the binary's symbol table. Since
all SSL symbols are stripped, the lookup returns offset 0 and libbpf rejects it.
Verify: Are SSL symbols really stripped?
# Check static symbol table $ readelf -s ~/.local/share/claude/versions/2.1.39 | grep -i "ssl_write\|ssl_read" # (no output) # Check dynamic symbol table $ readelf --dyn-syms ~/.local/share/claude/versions/2.1.39 | grep -i ssl # (no output) # Use gdb to search $ sudo gdb -batch -p 847282 -ex "info functions SSL" # All functions matching regular expression "SSL": # (no results) Confirmed: All SSL/BoringSSL symbols are completely stripped from the binary.
Phase 3: Identifying the Runtime
Discovering Bun
# Search for runtime identification strings $ strings ~/.local/share/claude/versions/2.1.39 | grep "node_module_register" node_module_register # → Uses Node.js API $ strings ~/.local/share/claude/versions/2.1.39 | grep "bun-vfs" /bun-vfs$$/node_modules/crypto/index.js # → Uses Bun virtual filesystem! $ strings ~/.local/share/claude/versions/2.1.39 | grep "Bun v" Bun v1.3.9-canary.51+d5628db23 (Linux x64 baseline) # → Exact Bun version identified Discovering BoringSSL
$ strings ~/.local/share/claude/versions/2.1.39 | grep "boring" BoringSSLError openssl_is_boringssl ../../../vendor/boringssl/ssl/ssl_buffer.cc ../../../vendor/boringssl/ssl/ssl_lib.cc ../../../vendor/boringssl/crypto/fipsmodule/bn/... # (many more BoringSSL source paths) Exported symbols confirm Bun
$ readelf -s ~/.local/share/claude/versions/2.1.39 | grep "BUN_1.2" | grep "FUNC" | wc -l 556 # 556 exported functions with BUN_1.2 version tag # Includes: uv_write, uv_read_start, napi_*, v8::* etc. Binary Properties Summary
| Property | Value |
|---|---|
| Binary path | ~/.local/share/claude/versions/2.1.39 |
| Symlink target | /home/yunwei37/.local/share/claude/versions/2.1.39 |
| File size | 222,867,057 bytes (~213 MB) |
| Runtime | Bun v1.3.9-canary.51+d5628db23 |
| Build variant | Linux x64 baseline |
| SSL library | BoringSSL (statically linked, fully stripped) |
| Exported symbols | 556 functions (BUN_1.2), 981 dynamic symbols total |
| USDT probes | 3 (libstdcxx: catch, throw, rethrow — not useful for SSL) |
Phase 4: Finding BoringSSL Function Offsets
Strategy: Cross-reference with Bun profile build
Since Bun is open-source, release builds include a "profile" variant with
debug symbols. The function code is identical between profile and stripped
builds; only symbols differ.
Step 1: Download the matching profile build
# List available assets for bun v1.3.9 $ gh release view bun-v1.3.9 --repo oven-sh/bun --json assets \ --jq '.assets[].name' | grep linux-x64 bun-linux-x64-baseline-profile.zip # <-- This one (Claude uses "baseline") bun-linux-x64-baseline.zip bun-linux-x64-profile.zip bun-linux-x64.zip # Download the baseline profile build $ gh release download bun-v1.3.9 --repo oven-sh/bun \ --pattern "bun-linux-x64-baseline-profile.zip" $ unzip bun-linux-x64-baseline-profile.zip Step 2: Extract SSL function symbols from the profile build
$ readelf -s bun-profile | grep -E " SSL_write$| SSL_read$| SSL_do_handshake$" 97928: 0000000005f3fe00 379 FUNC LOCAL HIDDEN 16 SSL_write 97985: 0000000005f3ea70 1506 FUNC LOCAL HIDDEN 16 SSL_do_handshake 97988: 0000000005f3f160 247 FUNC LOCAL HIDDEN 16 SSL_read Step 3: Calculate file offsets from virtual addresses
The profile build's ELF program headers:
LOAD offset=0x000000 VA=0x200000 size=0x28dfef0 (R) ← data LOAD offset=0x28dff00 VA=0x2ae0f00 size=0x38c80f0 (R E) ← code Conversion formula: file_offset = (VA - seg_VA) + seg_file_offset
# Profile build function file offsets: SSL_write: VA=0x5f3fe00 → FileOff=0x5d3ee00 SSL_read: VA=0x5f3f160 → FileOff=0x5d3e160 SSL_do_handshake: VA=0x5f3ea70 → FileOff=0x5d3da70 Step 4: Extract function byte prologues
$ xxd -s 0x5d3ee00 -l 48 bun-profile # SSL_write 05d3ee00: 5548 89e5 4157 4156 4155 4154 5348 83ec 05d3ee10: 1841 89d7 4989 f648 89fb 488b 4730 c780 $ xxd -s 0x5d3e160 -l 48 bun-profile # SSL_read 05d3e160: 5548 89e5 4157 4156 5350 4883 bf98 0000 05d3e170: 0000 742b baf3 5a31 00bf 1000 0000 be42 $ xxd -s 0x5d3da70 -l 48 bun-profile # SSL_do_handshake 05d3da70: 5548 89e5 4157 4156 4155 4154 5348 83ec 05d3da80: 2849 89fc 488b 4730 c780 c400 0000 0000 Step 5: Search for patterns in the Claude binary
# Python script using mmap to search for byte patterns # SSL_write pattern (26 bytes): # 55 48 89 e5 41 57 41 56 41 55 41 54 53 48 83 ec # 18 41 89 d7 49 89 f6 48 89 fb # # Result: 13 matches (many false positives from common prologues) # Last match at 0x5c39b20 — closest to SSL_read match # SSL_read pattern (19 bytes): # 55 48 89 e5 41 57 41 56 53 50 48 83 bf 98 00 00 # 00 00 74 # # Result: 1 match at 0x5c38e80 ← unique! # SSL_do_handshake pattern (24 bytes): # 55 48 89 e5 41 57 41 56 41 55 41 54 53 48 83 ec # 28 49 89 fc 48 8b 47 30 # # Result: 1 match at 0x5c38790 ← unique! Step 6: Validate with relative distances
=== Profile build relative distances === read - handshake = 0x6F0 (1776 bytes) write - read = 0xCA0 (3232 bytes) === Claude binary relative distances === read - handshake = 0x6F0 (1776 bytes) ← IDENTICAL write - read = 0xCA0 (3232 bytes) ← IDENTICAL *** PERFECT MATCH *** Step 7: Verify byte-level match
# Compare 128 bytes of SSL_write between profile and Claude $ xxd -s 0x5d3ee00 -l 128 bun-profile $ xxd -s 0x5c39b20 -l 128 claude-binary # Result: nearly identical! Only address references differ # (expected, since binaries are linked at different addresses) Final verified offsets in Claude binary
SSL_do_handshake: 0x5c38790 SSL_read: 0x5c38e80 SSL_write: 0x5c39b20 Phase 5: Implementing Byte-Pattern Detection
Changes to sslsniff.c
Added three components:
1. Offset-based uprobe macros
#define __ATTACH_UPROBE_OFFSET(skel, binary_path, offset, prog_name, is_retprobe) \ do { \ LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, .retprobe = is_retprobe); \ skel->links.prog_name = bpf_program__attach_uprobe_opts( \ skel->progs.prog_name, env.pid, binary_path, offset, &uprobe_opts); \ } while (false) Key difference from symbol-based macro: func_name is omitted (NULL), and
the offset parameter is passed directly as the file offset.
2. find_boringssl_offsets() function
- Opens the target binary with
mmap() - Searches for three unique byte patterns (SSL_read → SSL_do_handshake → SSL_write)
- Validates matches using known relative distances (0x6F0 and 0xCA0)
- Falls back to broader search if distances don't match
3. Two-stage --binary-path handler
// First: try symbol-based attachment struct bpf_link *test_link = bpf_program__attach_uprobe_opts(..., "SSL_write", ...); if (test_link) { // Standard path: symbols available attach_openssl(obj, env.extra_lib); } else { // Fallback: pattern detection for stripped binaries struct boringssl_offsets offsets = find_boringssl_offsets(env.extra_lib); if (offsets.found) { attach_openssl_by_offset(obj, env.extra_lib, &offsets); } } Build & Test
$ make -C bpf # Compiles successfully with no errors $ cd collector && cargo build --release # Builds successfully $ cd bpf && make test # 24/24 C tests passed $ cd collector && cargo test # 89/89 Rust tests passed Phase 6: Testing — Partial Success
Verbose output confirms BoringSSL detection
$ sudo ./bpf/sslsniff --binary-path ~/.local/share/claude/versions/2.1.39 --verbose # stderr output: Attaching to binary: /home/yunwei37/.local/share/claude/versions/2.1.39 Symbols not found, trying BoringSSL pattern detection... BoringSSL detected in /home/yunwei37/.local/share/claude/versions/2.1.39: SSL_do_handshake offset: 0x5c38790 SSL_read offset: 0x5c38e80 SSL_write offset: 0x5c39b20 BoringSSL detected! Attaching by offset... First capture: telemetry traffic
$ sudo ./bpf/sslsniff --binary-path ~/.local/share/claude/versions/2.1.39 Captured 3 events within seconds:
{"function":"WRITE/SEND","comm":"HTTP Client","pid":847282, "data":"GET /api/hello HTTP/1.1\r\nHost: api.anthropic.com\r\nUser-Agent: axios/1.8.4\r\n\r\n"} {"function":"READ/RECV","comm":"HTTP Client","pid":847282, "data":"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n..."} {"function":"READ/RECV","comm":"HTTP Client","pid":847282, "data":"0\r\n\r\n"} Extended capture (8 seconds): more traffic including event logging
Captured POST to /api/event_logging/batch with full telemetry data including:
- Session IDs, device IDs
- Event types:
tengu_permission_request_option_selected,tengu_unary_event, etc. - Model information:
claude-opus-4-6 - Growthbook experiment events
- Cost threshold events
Failure: --comm claude filter doesn't work
$ sudo ./bpf/sslsniff -c claude --binary-path ~/.local/share/claude/versions/2.1.39 # NO OUTPUT for 20 seconds Root cause: SSL traffic comes from thread named HTTP Client, not claude.
The eBPF bpf_get_current_comm() returns the thread name, not the process
name. Bun's HTTP client runs in a separate thread.
Fix: Omit the -c filter when monitoring Claude, or the collector should
not pass --comm for the SSL runner when using --binary-path.
Phase 7: The Two-Path TLS Problem
Discovery: No /v1/messages API calls captured
After 60 seconds of monitoring during active conversation:
$ sudo ./bpf/sslsniff --binary-path ~/.local/share/claude/versions/2.1.39 \ 2>/dev/null > /tmp/capture.log $ sleep 60 # Result: 92 lines captured # Check endpoints: $ python3 -c "..." /tmp/capture.log # Unique endpoints seen: # GET /api/hello # POST /api/event_logging/batch # POST /api/v2/logs # # Messages API check: $ grep -c "messages\|v1/message" /tmp/capture.log # 0 ← NONE! The actual conversation API (/v1/messages) was never captured.
Thread analysis reveals the cause
$ for tid in $(ls /proc/847282/task/); do comm=$(cat /proc/847282/task/$tid/comm) echo "TID=$tid comm=$comm" done TID=847282 comm=claude TID=847284 comm=claude TID=847285-847291 comm=HeapHelper (7 threads) TID=847297 comm=HTTP Client # ← axios telemetry uses this TID=847298-847333 comm=Bun Pool 0-6 TID=847323 comm=File Watcher TID=890093+ comm=JITWorker All captured traffic came from TID=847297 (HTTP Client thread) via the
axios/1.8.4 HTTP client library. The main conversation API uses Bun's
native fetch() which goes through a completely different TLS path.
Root cause: Bun's dual TLS architecture
Bun has two independent TLS implementations:
-
Node.js compatibility path (used by
axios,node:https):- Goes through standard BoringSSL
SSL_write/SSL_read - Runs on the
HTTP Clientthread - Our hooks capture this path ✓
- Goes through standard BoringSSL
-
Bun native path (used by
fetch(), Bun's HTTP client):- Uses uSockets library with custom BIO callbacks
- Does NOT call
SSL_write/SSL_read - Uses
ssl_on_datacallback for received data - Runs on the main
claudethread - Our hooks do NOT capture this path ✗
uSockets function analysis
From the Bun profile build:
$ readelf -W -s bun-profile | grep -E "ssl_on_|BIO_s_custom|us_internal_ssl" 48809: 040f6cc0 18 FUNC LOCAL BIO_s_custom_create 48810: 040f6cf0 74 FUNC LOCAL BIO_s_custom_write # encrypted data out 48811: 040f6d40 101 FUNC LOCAL BIO_s_custom_read # encrypted data in 48812: 040f6ce0 14 FUNC LOCAL BIO_s_custom_ctrl 48825: 040f77b0 732 FUNC LOCAL ssl_on_data # plaintext data received 48826: 040f7a90 164 FUNC LOCAL ssl_on_writable # writable notification 48831: 040f6db0 417 FUNC LOCAL ssl_on_open # connection opened 48824: 040f7690 136 FUNC LOCAL ssl_on_close # connection closed uSockets functions located in Claude binary
Using 32-byte pattern matching with 64-byte verification:
# Search results (verified with 64-byte similarity check): ssl_on_data: claude offset=0x3dde620 (64/64 bytes match) ssl_on_writable: claude offset=0x3dde900 (63/64 bytes match) ssl_on_open: claude offset=0x3dddc20 (64/64 bytes match) ssl_on_close: claude offset=0x3dde500 (60/64 bytes match) BIO_s_custom_write: claude offset=0x3dddb60 (64/64 bytes match) BIO_s_custom_read: claude offset=0x3dddbb0 (63/64 bytes match) Important: Initial pattern search for ssl_on_data returned a false
positive at offset 0x27d8050 (only 25% byte match). The prologue
55 48 89 e5 41 57 41 56 41 55 41 54 53 48 83 ec is extremely common
across all functions. Using 32+ byte patterns with 64-byte validation
eliminated the false positive.
Failed attempt: bpftrace verification
$ sudo bpftrace -e ' uprobe:/home/yunwei37/.local/share/claude/versions/2.1.39:0x3dde620 { ... } ' -c "sleep 10" # ERROR: Could not resolve address: ...0x3dde620 bpftrace was unable to resolve the offset. This may be a bpftrace limitation
with large stripped ELF executables. The libbpf-based sslsniff approach
(using bpf_program__attach_uprobe_opts directly) works correctly for the
BoringSSL hooks, suggesting the same approach would work for uSockets hooks.
Current Status & Next Steps
Current status
| Component | Status |
|---|---|
| BoringSSL pattern detection | ✅ Working |
| sslsniff offset-based attachment | ✅ Working |
| Telemetry/heartbeat capture | ✅ Working |
/v1/messages (prompt) capture | ✅ Working (see Phase 9) |
| Collector integration | ✅ --binary-path passed through |
How to capture Claude traffic
# Start monitoring BEFORE submitting a message to Claude: sudo ./bpf/sslsniff --binary-path ~/.local/share/claude/versions/2.1.39 # The --comm filter does NOT work because traffic comes from "HTTP Client" thread # Do NOT use: sudo ./bpf/sslsniff -c claude --binary-path ... All traffic (telemetry + conversation API) flows through the HTTP Client
thread via BoringSSL. The capture must be running when a new message is
submitted to capture the POST /v1/messages request and SSE response stream.
Remaining work
Collector: FIXED —--commfilter fixcollector/src/main.rs
now skips passing--commto sslsniff when--binary-pathis specified.
The process runner still receives--commfor process monitoring.HTTP response decompression: Responses may use
br(Brotli) orgzip
encoding. The collector's HTTP decompressor should handle these.Large message reassembly: Conversation context can be very large (the
full message history is sent with each request). The 512KB MAX_BUF_SIZE
in sslsniff.h may truncate large requests. Multiple SSL_write calls may
need to be reassembled.
Files modified
| File | Changes |
|---|---|
bpf/sslsniff.c | Added find_boringssl_offsets(), attach_openssl_by_offset(), offset-based uprobe macros, two-stage --binary-path handler |
collector/src/main.rs | Fixed --comm filter: skip passing -c to sslsniff when --binary-path is specified (SSL traffic uses "HTTP Client" thread name, not process name) |
docs/claude-code-analysis.md | This document |
Appendix: Claude Binary Architecture
ELF Program Headers
Type Offset VirtAddr FileSiz MemSiz Flg LOAD 0x000000 0x0000000000200000 0x27d6908 0x27d6908 R ← rodata LOAD 0x27d6c00 0x0000000002ae7c00 0x38cc010 0x38cc010 R E ← code LOAD 0x60a2c10 0x00000000063b4c10 0x08fd88 0x1753e0 RW ← data/bss Entry point: 0x2ae7c00 Thread Model (full)
| Thread Name | Count | Purpose |
|---|---|---|
claude | 2 | Main thread + event loop |
HeapHelper | 7 | Garbage collection assistance |
HTTP Client | 1 | axios HTTP client (telemetry, heartbeat) |
Bun Pool 0-6 | 7 | Bun thread pool (async I/O) |
File Watcher | 1 | File system monitoring |
JITWorker | 2-3 | Just-in-time compilation |
Network Endpoints Observed
| Host | Endpoint | Method | Purpose | Client |
|---|---|---|---|---|
| api.anthropic.com | /api/hello | GET | Heartbeat | axios |
| api.anthropic.com | /api/event_logging/batch | POST | Telemetry | axios |
| api.anthropic.com | /v1/messages | POST | Conversation | Bun fetch |
| http-intake.logs.us5.datadoghq.com | /api/v2/logs | POST | Datadog logging | axios |
Multi-Process Architecture
Claude Code runs multiple processes:
$ pgrep -a claude 847282 claude # Main session (19-21 threads, active conversation) 890428 claude # Sub-process (20 threads, also sends telemetry) 269932 claude # Other user's session Both PID 847282 and 890428 emit telemetry traffic via their respective
HTTP Client threads. The sub-process (890428) appears to handle some
independent tasks.
Key HTTP Headers
Authorization: Bearer sk-ant-oat01-... Content-Type: application/json User-Agent: claude-code/2.1.39 anthropic-beta: oauth-2025-04-20 x-service-name: claude-code Telemetry payloads include: session_id, device_id, model name, event types
(permission requests, accept/submit events, cost thresholds, Growthbook
experiments), platform info (linux, node v24.3.0, is_running_with_bun: true).
Phase 8: Deep Investigation of the Native Fetch TLS Path
Goal
Determine exactly how Bun's native fetch() handles TLS encryption for the
/v1/messages API, since BoringSSL SSL_read/SSL_write hooks only capture
the axios telemetry path.
Step 1: Verify ssl_on_data is NOT the native fetch path
We confirmed via disassembly that ssl_on_data in the Claude binary calls
SSL_read at exactly our hooked offset (0x5c38e80):
# Profile build: ssl_on_data calls SSL_read at offset +0xA1 40f7851: e8 0a 79 e4 01 call 5f3f160 <SSL_read> # Claude binary: same relative call at ssl_on_data + 0xA1 03dde6c1: e8 ba a7 e5 01 → target = 0x5c38e80 ✓ (matches our hook) But when traced with ftrace uprobes, ssl_on_data only fires from the
HTTP Client thread (axios), never from the main thread:
$ sudo bash -c 'echo "p:my_ssl_on_data /path/to/claude:0x3dde620" > \ /sys/kernel/tracing/uprobe_events' $ sudo bash -c 'echo 1 > /sys/kernel/tracing/events/uprobes/my_ssl_on_data/enable' $ sleep 3 # Result: ONLY HTTP Client thread HTTP Client-847297 [016] my_ssl_on_data: (0x40ef620) HTTP Client-847297 [016] my_ssl_on_data: (0x40ef620) HTTP Client-890449 [017] my_ssl_on_data: (0x40ef620) Step 2: Confirm SSL_read is only called from HTTP Client
Registered a uprobe directly on SSL_read (offset 0x5c38e80) and traced for
10 seconds during active conversation:
$ sudo bash -c 'echo "p:ssl_read_trace /path/to/claude:0x5c38e80" > \ /sys/kernel/tracing/uprobe_events' # ... enable and wait 10 seconds ... # Result: 18 calls, ALL from HTTP Client 18 HTTP Client No SSL_read calls from the main claude thread, Bun Pool threads, or
any other thread.
Step 3: Check for multiple copies of SSL_read
Searched the entire Claude binary for the SSL_read byte-pattern prologue:
pattern = bytes([0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56, 0x53, 0x50, 0x48, 0x83, 0xbf, 0x98, 0x00, 0x00, 0x00, 0x00, 0x74]) # Result: Only 1 match at offset 0x5c38e80 # There is NO second copy of SSL_read in the binary. Step 4: Find ALL callers of SSL_read and SSL_write
Scanned the entire code section of the Bun profile build for e8 (CALL)
instructions targeting SSL_read (VA 0x5f3f160) and SSL_write (VA 0x5f3fe00):
=== Functions calling SSL_read (2 call sites) === 1. VA=0x369a330 → SSLWrapper.handleTraffic (ssl_wrapper.zig:491) 2. VA=0x40f7851 → ssl_on_data (uSockets callback) === Functions calling SSL_write (4 call sites) === 1. VA=0x34fa553 → http.ProxyTunnel.write (ProxyTunnel.zig:351) 2. VA=0x35c9308 → deps.uws.UpgradedDuplex.encodeAndWrite (UpgradedDuplex.zig:356) 3. VA=0x3a42208 → http.websocket_client.WebSocketProxyTunnel.write 4. VA=0x40fde0e → us_socket_write (uSockets C library) Step 5: Verify SSLWrapper.handleTraffic is NOT called
Located SSLWrapper.handleTraffic in the Claude binary at file offset
0x3390c30 (pattern-matched from profile build). Verified it also calls
SSL_read at the correct offset. Then traced it:
$ sudo bash -c 'echo "p:ssl_wrapper_trace /path/to/claude:0x3390c30" > \ /sys/kernel/tracing/uprobe_events' # ... enabled for 10 seconds ... # Result: 0 calls. SSLWrapper.handleTraffic is NEVER invoked. Step 6: Verify BIO_s_custom_write/read are NOT used by native fetch
Traced the uSockets custom BIO functions:
# BIO_s_custom_write at offset 0x3dddb60 # BIO_s_custom_read at offset 0x3dddbb0 # Result after 5 seconds: === BIO_s_custom_write calls === 7 HTTP Client === BIO_s_custom_read calls === 5 HTTP Client All BIO custom callbacks also only fire from HTTP Client thread.
Step 7: Investigate kernel TLS (kTLS)
Checked if kTLS was the mechanism bypassing SSL_read/SSL_write:
$ lsmod | grep tls tls 155648 26 # Module loaded! $ cat /proc/net/tls_stat TlsCurrTxSw 0 TlsCurrRxSw 0 TlsTxSw 0 TlsRxSw 0 # All counters are 0 → kTLS is NOT actively used kTLS ruled out. Module is loaded but no connections use it.
Step 8: Trace network syscalls from Claude
Main thread (TID=959023) syscalls
The main Claude thread only performs:
-
read(fd=6, count=8)— eventfd (event loop notification) -
read(fd=7, count=8)— timerfd (timer events) -
write(fd=13, ...)— /dev/pts/7 (terminal output) -
write(fd=6, count=8)— eventfd (signaling event loop) - Occasional
write(fd=18, count=0x6c7)— socket to api.anthropic.com
FD 6 → anon_inode:[eventfd] (event loop) FD 7 → anon_inode:[timerfd] (timer) FD 8 → anon_inode:[timerfd] (timer) FD 13 → /dev/pts/7 (terminal) FD 14 → anon_inode:[eventpoll] (epoll) FD 17 → socket (api.anthropic.com:443) FD 18 → socket (api.anthropic.com:443) FD 19 → socket (api.anthropic.com:443) FD 25 → socket (api.anthropic.com:443) Key finding: The main thread does NO...

