Agents are hardwired to be prolific writers and readers of code. As my work on Bram progressed I found that their code-first instinct wasn’t serving me well. So I began pushing them to be, also, prolific writers and readers of logs.
Bram is a Tauri app, so it’s written in Rust. But it’s also a JavaScript app that hosts a terminal where Claude Code and Codex run, and it’s an XMLUI app that reimagines how to display and interact with those terminal-based agents, and it’s a workflow governed by a set of Markdown files and Python hooks. The app’s behavior arises from the dynamic interplay of these layers, languages, and components.
Was the right message sent to the agent at the right time? Did the rule-defined workflow transition occur? Did the agent’s response render correctly? These are observations about runtime behavior. When something goes wrong, the drill is now:
– Do we have the instrumentation to know what happened?
– If no, add it.
– If yes, use it.
This applies as much to developing new features as it does to debugging existing ones. For example, Bram tracks the TUI (text user interface) menus that Claude Code and Codex present, and renders them as GUI menus. It was arguably foolish to even try this kind of screenscraping. Web pages (when not delivered as minified JavaScript) have structure that, while prone to change, is easy to target. Tap into a TUI and you’re looking at a stream of content bytes intermixed with control characters. It’s the source of truth, but a hard one to reason about. So we began gathering evidence.
The ladder of evidence
JSONL session files are the final record. But it can take a few seconds for activity to show up there, and they mainly preserve conversation not interaction. So Bram recruits three other layers: PTY, grid, and hook.
PTY input
These are bytes read from the terminal process, i.e. what the TUI sent.
[2026-07-08T13:46:16.407Z] [pty-in] gap_ms=0 bytes=202 preview=”\x1b[?2026h\x1b[18;2H…”
Fields:
– gap_ms: milliseconds since the previous PTY input chunk.
– bytes: raw byte count for this chunk.
– runs: optional, count of repeated/compactable control runs.
– preview: escaped prefix of raw bytes. ANSI/control characters are preserved as escapes like \x1b, \r, \x07.
The xterm.js grid
The PTY stream isn’t just text, it’s an instruction set for painting a terminal: move the cursor, clear regions, set colors, write characters, update the title, enter or leave bracketed paste mode. Bram uses xterm.js to render those bytes to a terminal grid, then reads the resulting screen state.
– [grid-menu] op=report provider=claude count=3 parsed_offset=446235 [1.Yes | 2.Yes, and don’t ask again for: awk -F’]’ ‘$1 >= “[2026-07-07…”‘ | 3.No]
– [grid-menu] op=build-claude-nosig tool=Bash grid_count=3 cmd=”grep -E \”hook-menu|retire-suppressor\” bram-trace.log | tail…” grid=[1.Yes | 2.Yes, and don’t ask again for: … | 3.No]
The grid layer answers questions that raw PTY bytes cannot answer directly:
– What rows are visible right now?
– Which text is inside the permission box?
– Which option labels are present?
This is the layer where TUI screenscraping becomes tractable. It’s not regexes, it’s programmatic inspection of a reconstructed terminal screen.
PTY Output
These are bytes Bram writes into the terminal.
[2026-07-08T13:45:24.145Z] [pty-out] bytes=18 preview=”claude –continue\r” is_structured=false caller_hint=agent-autostart
Fields:
– bytes: number of bytes sent.
– preview: escaped text sent to the PTY.
– is_structured: whether it came from a structured Bram intent path (propose → apply → commit).
– caller_hint: why/where the write originated.
Hooks
Claude Code and Codex both fire lifecycle hooks when using menus to ask permission. Bram’s hook scripts relay those as structured JSON, timestamped into the same trace:
– [hook-menu] op=permission provider=claude tool=Edit options=3
– [hook-menu] op=payload tool=Edit body=”{\”tool_input\”:{\”file_path\”:\”src-tauri/src/lib.rs\”,\”old_string\”:…,\”new_string\”:…},\”permission_suggestions\”:[…]}”
– [worklist-guard] tool=Edit target=docs/esc-resend-redesign.md decision=deny reason=no-coverage-no-opt-out
The hook-menu trace reports a tool name, its full input, and the permission options the TUI is about to draw.
All the layers
PTY logs preserve messy reality: control bytes, cursor movement, bracketed paste markers, title updates, spinner frames. The grid layer turns that byte stream into visible terminal state. Hooks bypass reconstruction entirely, but only for some cases. The JSONL file describes final truth, but again only for some cases. Altogether the traces combine raw, reconstructed, and declared evidence. Interpretation taps into one or several of the layers as it needs to.
From evidence to construction
I can now mostly run Bram in GUI mode without looking at the terminal. Occasionally something gets stuck, so I’m toying with the notion of auto-opening the terminal when it needs attention. Is that reliably knowable? That wasn’t a question the logs could answer so I’ve added new instrumentation. After a day of normal use I’ll know whether the feature is even feasible, and if so, how an agent should build it.
Deciphering the traces
The schemas for these log entries have evolved organically. In the Before Time I’d have worried about that. Would the logs be amenable to structured query? If not, I’d need to write a one-off script to answer each question and that was unsustainable.
But for agents, writing one-off scripts is like breathing and Bram’s rendering makes that respiration more visible.
In “What is the terminal?” I showed how agents wield the repertoire of command-line tools to make your wishes come true. I see that happening constantly as they reach for awk, grep, sed, and perl to interpret Bram’s logs. Why awk or grep in one case, sed or perl in another? LLMs are nondeterministic but if there is logic that governs these choices I’d love to know what it is.
Baked-in log-first development
As this method evolved, Claude Code and Codex absorbed it into their stored memories. That was convenient, I could just ask “Do we have the instrumentation to support that?” and they’d do the right thing. But those memories aren’t shared between agents, never mind across the various repository-backed projects where Bram can run.
As I was writing this post I found that the log-first imperative was, in fact, only recorded in private agent memories. Now it’s baked into any project where Bram runs your agents.
