<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: YHH</title>
    <description>The latest articles on DEV Community by YHH (@esengine).</description>
    <link>https://dev.to/esengine</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3890710%2F36667688-2190-4222-a317-87b8f93e4306.jpeg</url>
      <title>DEV Community: YHH</title>
      <link>https://dev.to/esengine</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/esengine"/>
    <language>en</language>
    <item>
      <title>The boring secret to a cheap AI coding agent — a byte-stable prompt prefix</title>
      <dc:creator>YHH</dc:creator>
      <pubDate>Wed, 06 May 2026 01:18:07 +0000</pubDate>
      <link>https://dev.to/esengine/the-boring-secret-to-a-cheap-ai-coding-agent-a-byte-stable-prompt-prefix-5f7k</link>
      <guid>https://dev.to/esengine/the-boring-secret-to-a-cheap-ai-coding-agent-a-byte-stable-prompt-prefix-5f7k</guid>
      <description>&lt;p&gt;I tried using Claude Code for a few weeks and quietly stopped. Not because it was bad — it's great — but because I'd hesitate before kicking off any non-trivial task, doing the math on what a 30-minute debugging session would cost. That's the wrong incentive for an agent. The whole pitch of "let it run" depends on the bill not scaring you.&lt;/p&gt;

&lt;p&gt;So I went the other direction. I built &lt;a href="https://github.com/esengine/reasonix" rel="noopener noreferrer"&gt;Reasonix&lt;/a&gt; — same shape as Claude Code or Aider, runs in your terminal, plan mode, tool calls, MCP — but it only talks to DeepSeek, and the entire loop is engineered around one invariant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The prompt prefix must be byte-identical to the previous turn's prefix.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. That's the whole architectural constraint. Everything else falls out of it.&lt;/p&gt;

&lt;p&gt;This post is about why that constraint matters, what silently breaks it, and what the loop ends up looking like when you take it seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanic
&lt;/h2&gt;

&lt;p&gt;DeepSeek's API has prefix caching. If the first N tokens of your request match a recent request byte-for-byte, those tokens are billed at roughly &lt;strong&gt;1/10th&lt;/strong&gt; the normal input price. Cache TTL is generous — minutes — long enough that within a single conversation turn you basically always hit it if you didn't break the prefix.&lt;/p&gt;

&lt;p&gt;Most providers have something like this now. What's different is the price ratio and the granularity. On DeepSeek, if your agent is built right, &lt;strong&gt;every turn after the first is mostly cached&lt;/strong&gt;, and a long session costs cents instead of dollars.&lt;/p&gt;

&lt;p&gt;The catch: "built right" turns out to be load-bearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What silently breaks the cache
&lt;/h2&gt;

&lt;p&gt;These are the things I found that quietly destroy your cache hit rate. None of them throw errors. Your agent works fine. You just pay full price every turn and don't know why.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Non-deterministic JSON.stringify of tool schemas
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Looks fine. Is poison.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edit_file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;someObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JSON.stringify&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; guarantee key order across runs in all engines, and even when it does, your &lt;code&gt;someObject&lt;/code&gt; may have been built from an object spread that depends on insertion order. One reordered key — &lt;code&gt;{path, content}&lt;/code&gt; vs &lt;code&gt;{content, path}&lt;/code&gt; — and the cached prefix is gone.&lt;/p&gt;

&lt;p&gt;Fix: serialize tool schemas with a deterministic stringifier (sorted keys), and freeze the output. Once at startup, never re-serialize per turn.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Timestamps or run IDs in the system prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are a coding agent. Session: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Started: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks harmless. Destroys cache on every single turn because the prefix differs every run.&lt;/p&gt;

&lt;p&gt;Fix: nothing variable goes into the system prompt. Session metadata, if you really need the model to see it, goes into the &lt;em&gt;first user message&lt;/em&gt; — which is fine because that's already turn-1-only content.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Re-rendering tool results with one whitespace difference
&lt;/h3&gt;

&lt;p&gt;This one is sneaky. You call a tool, get a result, format it into a message, send it back. Next turn, you re-render the &lt;em&gt;same&lt;/em&gt; tool result from your event log — but a pretty-printer adds a trailing newline this time, or strips one. Cache gone.&lt;/p&gt;

&lt;p&gt;Fix: format tool results once, store the exact rendered string, append-only. Never re-derive past content from upstream sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. In-place edits to message history
&lt;/h3&gt;

&lt;p&gt;Summarization, truncation, "let me clean up this old turn so we fit in the context window" — every one of these mutates the prefix. Even if the &lt;em&gt;new&lt;/em&gt; shortened version is what you want the model to see, the cache was built against the &lt;em&gt;old&lt;/em&gt; version.&lt;/p&gt;

&lt;p&gt;Fix: history is append-only. If you need to compress old context, do it as a new turn ("here's a summary, ignore turns 1–8"), not as an in-place edit.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Switching tool definitions mid-session
&lt;/h3&gt;

&lt;p&gt;Adding a tool, removing one, even reordering the tools array — all of these change the system message that includes the tool schemas, which is part of the prefix.&lt;/p&gt;

&lt;p&gt;Fix: pin the tool set at session start. If you need dynamic tools, accept the cache miss as a deliberate event and surface it in your cost dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What doesn't break it
&lt;/h2&gt;

&lt;p&gt;For completeness, things I worried about that turned out to be fine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming vs non-streaming&lt;/strong&gt; — same prefix, same cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function-calling format vs JSON-tool-call format&lt;/strong&gt; — pick one and stick, but either works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding new turns&lt;/strong&gt; — obviously, that's the entire point. Append is free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool result &lt;em&gt;content&lt;/em&gt; changing turn-to-turn&lt;/strong&gt; — only the &lt;em&gt;formatting&lt;/em&gt; of past results matters. Future tool calls returning different data is normal and doesn't touch the prefix.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the loop looks like when you take this seriously
&lt;/h2&gt;

&lt;p&gt;Reasonix's loop is built backward from the byte-stability requirement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Turn N+1 prefix = Turn N prefix + (assistant turn N) + (user turn N+1)
                                  ↑                    ↑
                          rendered once,        rendered once,
                          stored as string      stored as string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole shape. There is no code path anywhere in the loop that re-derives past content from upstream sources at request time. Past content is &lt;strong&gt;strings, in an array, appended to&lt;/strong&gt;. Period.&lt;/p&gt;

&lt;p&gt;A few specific design consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System prompt is a constant.&lt;/strong&gt; Compiled at startup, frozen. No template variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool schemas are serialized once&lt;/strong&gt; with a sorted-key stringifier and concatenated into the system message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool results are formatted at the moment of receipt&lt;/strong&gt; and stored as the exact bytes that will be sent. The loop replays bytes, not objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no summarization step in the main loop.&lt;/strong&gt; When context gets large, the user can &lt;code&gt;/compact&lt;/code&gt; explicitly — which is a &lt;em&gt;new turn&lt;/em&gt; containing a summary, not an in-place rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions / plan mode / hooks&lt;/strong&gt; all operate on what the user sees, never on what gets sent to the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more restrictive than a generic agent framework wants to be. That's the point. The constraint is the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this gets you
&lt;/h2&gt;

&lt;p&gt;On a long debugging session — say, an hour of back-and-forth on a real codebase, 50–80 turns, lots of tool calls — Reasonix bills come in around &lt;strong&gt;5–15 cents&lt;/strong&gt; depending on how much code the model reads. The same session through a non-cache-aware framework on DeepSeek would be roughly $1–$3. Through Claude (Sonnet) it'd be $5–$15.&lt;/p&gt;

&lt;p&gt;The cheapness isn't the goal — the goal is changing the &lt;em&gt;posture&lt;/em&gt;. When a session costs cents, you stop curating prompts and start delegating real chunks of work. You leave it running while you go to lunch. That's the whole user-experience shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I'm honest about
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's DeepSeek-only.&lt;/strong&gt; That's a feature, not a bug — every layer is tuned to one provider's cache mechanic. But if DeepSeek goes down, you're down. I think the cost ratio is worth it; you may not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeepSeek is a Chinese provider.&lt;/strong&gt; Some companies can't use it. That's a real constraint, not something I can engineer around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R1's quality on agentic tool-use is a notch below Sonnet.&lt;/strong&gt; Closer than you'd expect, but it's there. The cost ratio still wins for me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The prefix-stability discipline is contagious&lt;/strong&gt; — once you've enforced it in the loop, you start noticing every place else in your stack that quietly mutates state for no reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx reasonix code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/esengine/reasonix" rel="noopener noreferrer"&gt;github.com/esengine/reasonix&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT, TypeScript, Node 22+. Works on macOS, Linux, Windows (PowerShell, Git Bash, Windows Terminal).&lt;/p&gt;

&lt;p&gt;Architecture writeup with the four-pillar breakdown is in &lt;code&gt;docs/ARCHITECTURE.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you've measured cache hit rates in your own agent setup — generic framework or otherwise — I'd genuinely like to see numbers. The thing I can't tell from the outside is whether everyone is silently eating full-price tokens, or whether some of the popular frameworks have quietly fixed this and I missed it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Designing a Hooks System for AI Agent CLIs: 4 Lifecycle Points That Cover Everything</title>
      <dc:creator>YHH</dc:creator>
      <pubDate>Thu, 23 Apr 2026 10:38:50 +0000</pubDate>
      <link>https://dev.to/esengine/designing-a-hooks-system-for-ai-agent-clis-4-lifecycle-points-that-cover-everything-251g</link>
      <guid>https://dev.to/esengine/designing-a-hooks-system-for-ai-agent-clis-4-lifecycle-points-that-cover-everything-251g</guid>
      <description>&lt;h1&gt;
  
  
  Designing Hooks for an AI Agent CLI: 4 Lifecycle Points, Shell-Only, and What I Cut
&lt;/h1&gt;

&lt;p&gt;A few weeks ago I shipped &lt;a href="https://github.com/esengine/reasonix" rel="noopener noreferrer"&gt;Reasonix&lt;/a&gt; — a DeepSeek-native TypeScript agent framework. The first article was about the cache-first prompt structure that pushed cache hit rates to 85-95% on real sessions. That got picked up on dev.to.&lt;/p&gt;

&lt;p&gt;Two weeks later, I needed hooks. Specifically I wanted my coding agent to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refuse &lt;code&gt;rm -rf&lt;/code&gt; even if the model decided that was a great idea&lt;/li&gt;
&lt;li&gt;Auto-format files after every edit&lt;/li&gt;
&lt;li&gt;Auto-commit at the end of a session if the diff was clean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I could have hardcoded each of these. But every time I did, three days later I'd want a fourth thing — and the agent would grow a "configuration ballast" problem.&lt;/p&gt;

&lt;p&gt;So I designed a hooks system. This post is the design doc, including what I cut and the bugs that shaped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hooks at all (and not the obvious alternatives)
&lt;/h2&gt;

&lt;p&gt;Three things that almost made it in but didn't:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A TypeScript plugin system.&lt;/strong&gt;&lt;br&gt;
You'd &lt;code&gt;npm install reasonix-hook-prettier&lt;/code&gt;, the framework would import it, hooks would be JS callbacks. I rejected this because hooks are a sysadmin concern, not a JS concern. The user who wants "run prettier after every edit" already has prettier on &lt;code&gt;$PATH&lt;/code&gt; and knows how to invoke it. Forcing them through &lt;code&gt;npm&lt;/code&gt; and a TS API is gatekeeping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A middleware chain like Express.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;loop.use((event, next) =&amp;gt; ...)&lt;/code&gt;. I rejected this because hooks aren't transformers. They observe, and sometimes veto. Middleware semantics imply "you can rewrite the payload and pass it on" — a much bigger contract than what most users actually want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Webhooks (HTTP callouts).&lt;/strong&gt;&lt;br&gt;
Too much infrastructure. "After every edit, run prettier" should not require standing up an HTTP server.&lt;/p&gt;

&lt;p&gt;What won: &lt;strong&gt;a hook is a shell command&lt;/strong&gt;. Reasonix invokes it with a JSON payload on stdin and reads the exit code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.reasonix/settings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit_file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier --write &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$(jq -r .toolArgs.path)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Language-agnostic. Composable with everything in the user's terminal. Mental model is identical to "what would I run in my shell?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 events — split into 2 categories
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Fires when&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PreToolUse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The model decided to call a tool, before dispatch&lt;/td&gt;
&lt;td&gt;Gating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PostToolUse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A tool returned, before the result reaches the model&lt;/td&gt;
&lt;td&gt;Observing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserPromptSubmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The user typed a prompt, before it goes to the model&lt;/td&gt;
&lt;td&gt;Gating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The loop finished a turn (assistant returned final text)&lt;/td&gt;
&lt;td&gt;Observing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two categories, not four. The split drives every other decision in the design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BLOCKING_EVENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReadonlySet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HookEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PreToolUse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UserPromptSubmit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_TIMEOUTS_MS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HookEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;PreToolUse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;UserPromptSubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;PostToolUse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gating events&lt;/strong&gt; can refuse to let the loop proceed (exit code 2 = block). They have a tight 5-second timeout because they hold up forward progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observing events&lt;/strong&gt; fire after the action is already done, so blocking is meaningless — &lt;code&gt;exit 2&lt;/code&gt; from a &lt;code&gt;PostToolUse&lt;/code&gt; hook just becomes a warning. They get 30 seconds because nobody is waiting on them.&lt;/p&gt;

&lt;p&gt;This asymmetry is hardcoded in the decision matrix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decideOutcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HookEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HookSpawnResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spawnError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timedOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;BLOCKING_EVENTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pass&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;BLOCKING_EVENTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Events I cut
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SessionStart / SessionEnd&lt;/strong&gt; — solved by the user's shell. If you want to run something before &lt;code&gt;reasonix chat&lt;/code&gt;, you write &lt;code&gt;pre-reasonix &amp;amp;&amp;amp; reasonix chat&lt;/code&gt;. I'm not the right system to schedule that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PreCompact&lt;/strong&gt; — Reasonix doesn't do automatic context compaction (the cache-first design works against compaction). No event to hook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-token streaming&lt;/strong&gt; — would have been a hook firing for every token of model output. Use cases (PII redaction, profanity filter) exist, but the per-call cost would be brutal and post-processing the final text is more sensible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OnError&lt;/strong&gt; — error handling lives at the loop level; a hook can't meaningfully recover from "the model returned malformed JSON." A subprocess can't fix what only the loop sees.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The protocol — JSON in, exit code out
&lt;/h2&gt;

&lt;p&gt;Every hook gets a single-line JSON envelope on stdin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/Users/me/my-project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"toolName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit_file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"toolArgs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src/foo.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fields differ by event. &lt;code&gt;PostToolUse&lt;/code&gt; adds &lt;code&gt;toolResult&lt;/code&gt;. &lt;code&gt;UserPromptSubmit&lt;/code&gt; has &lt;code&gt;prompt&lt;/code&gt;. &lt;code&gt;Stop&lt;/code&gt; has &lt;code&gt;lastAssistantText&lt;/code&gt; and &lt;code&gt;turn&lt;/code&gt;. The shape is documented in &lt;a href="https://github.com/esengine/reasonix/blob/main/src/hooks.ts" rel="noopener noreferrer"&gt;hooks.ts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Exit code is the protocol:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;Gating event&lt;/th&gt;
&lt;th&gt;Observing event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pass (continue)&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;block (stop the chain)&lt;/td&gt;
&lt;td&gt;warn (action already done)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anything else&lt;/td&gt;
&lt;td&gt;warn (continue, log)&lt;/td&gt;
&lt;td&gt;warn&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why exit code instead of structured stdout JSON?&lt;/strong&gt; Because I wanted hooks to be writable in one line of bash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; .git/MERGE_HEAD &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;2  &lt;span class="c"&gt;# block any tool call during a merge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Versus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; .git/MERGE_HEAD &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"decision":"block","reason":"merge in progress"}'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"decision":"pass"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second is cleaner from a typed-API perspective and worse from a "I'll write this in 30 seconds" perspective. I optimized for 30 seconds.&lt;/p&gt;

&lt;p&gt;The cost: hooks can't return structured data back to the model. A &lt;code&gt;PreToolUse&lt;/code&gt; hook can only veto or pass — it can't say "let me rewrite these args first." That's a real limitation. The upgrade path, if demand grows, is opt-in: a &lt;code&gt;parseStdoutAsJson: true&lt;/code&gt; flag in settings. For now, not worth the complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two surprising design calls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Call 1 — &lt;code&gt;match&lt;/code&gt; is anchored regex, not substring
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;matchesTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResolvedHook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PreToolUse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PostToolUse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;re&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`^(?:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)$`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// fail closed — see "Bugs that shaped the design"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;"match": "file"&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; trigger on &lt;code&gt;read_file&lt;/code&gt;. You have to write &lt;code&gt;".*file"&lt;/code&gt; or &lt;code&gt;"read_file|write_file|edit_file"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I tried substring first. Within a day I had a hook configured as &lt;code&gt;match: "edit"&lt;/code&gt; firing on &lt;code&gt;tool_called_edit_text&lt;/code&gt;, &lt;code&gt;git_edit_commit&lt;/code&gt;, and &lt;code&gt;audit_log_edit&lt;/code&gt; — all unintentional. Substring matching is intuitive in the small, dangerous in the large.&lt;/p&gt;

&lt;p&gt;The cost: more typing. The benefit: when you write &lt;code&gt;match: "shell"&lt;/code&gt;, that's exactly what fires. No surprises.&lt;/p&gt;

&lt;p&gt;I expect this to be the most-debated decision in this post. Feel free to argue with me in the comments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Call 2 — events fire from two different layers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/loop.ts        → fires PreToolUse, PostToolUse
src/cli/ui/App.tsx → fires UserPromptSubmit, Stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop is a library. It runs headless, in tests, in scripts. It doesn't know what a "user prompt submission" is — it just knows about &lt;code&gt;step(text)&lt;/code&gt;. The text could come from a TUI, a JSON RPC call, or a &lt;code&gt;for&lt;/code&gt; loop in a script.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;UserPromptSubmit&lt;/code&gt; lives at the App boundary — the TUI is what decides "the user just hit enter on this." Same for &lt;code&gt;Stop&lt;/code&gt; — the loop emits &lt;code&gt;assistant_final&lt;/code&gt; events, but "the turn is done from the user's perspective" is a UI concept.&lt;/p&gt;

&lt;p&gt;Practically: if you embed &lt;code&gt;CacheFirstLoop&lt;/code&gt; in your own app, you get tool-related hooks for free. You wire prompt and stop hooks yourself if you want them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Embedded usage — tool hooks "just work"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CacheFirstLoop&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hooks&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// You decide what counts as a "submitted prompt"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promptReport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runHooks&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UserPromptSubmit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Slightly more boilerplate. Way cleaner separation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real configurations that do useful things
&lt;/h2&gt;

&lt;p&gt;All copy-pasteable. Drop them in &lt;code&gt;.reasonix/settings.json&lt;/code&gt; (project) or &lt;code&gt;~/.reasonix/settings.json&lt;/code&gt; (global).&lt;/p&gt;

&lt;h3&gt;
  
  
  Block dangerous shell commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jq -r .toolArgs.command | grep -qE '^(rm -rf|sudo|curl.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;| bash)' &amp;amp;&amp;amp; { echo 'denied: dangerous command' &amp;gt;&amp;amp;2; exit 2; } || exit 0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block rm -rf / sudo / curl|bash"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;gt;&amp;amp;2&lt;/code&gt; matters — Reasonix surfaces stderr as the block reason in both the UI and the tool result the model sees. The model gets a structured refusal, not silence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-format after edits
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit_file|write_file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"path=$(jq -r .toolArgs.path) &amp;amp;&amp;amp; prettier --write &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$path&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null || true"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;|| true&lt;/code&gt; is intentional — if prettier doesn't recognize the file type, the hook still passes. We don't want a yellow warning row for every &lt;code&gt;.txt&lt;/code&gt; edit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Daily cost ceiling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"UserPromptSubmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$(reasonix stats --today --json | jq .totalCostUsd)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt; 5.00 || { echo 'daily budget hit ($5)' &amp;gt;&amp;amp;2; exit 2; }"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stop accepting prompts after $5/day"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Composes the &lt;code&gt;/stats&lt;/code&gt; feature with the hooks system. &lt;code&gt;reasonix stats --today --json&lt;/code&gt; returns running cost; the hook compares and blocks. Zero code changes to Reasonix needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-commit on Stop, only if clean
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git diff --quiet || (git add -A &amp;amp;&amp;amp; git commit -m &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;reasonix: $(jq -r .lastAssistantText | head -c 60)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; --no-verify)"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you trust the agent end-to-end, this turns every chat into a commit. I personally don't enable it — but several Reasonix users do, and it's been stable enough to mention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs that shaped the design
&lt;/h2&gt;

&lt;p&gt;Three real bugs that turned into permanent design decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. SIGTERM doesn't always land on Windows shell children
&lt;/h3&gt;

&lt;p&gt;First version killed timed-out hooks with &lt;code&gt;child.kill("SIGTERM")&lt;/code&gt;. On Windows, when the hook ran through &lt;code&gt;cmd.exe /c&lt;/code&gt;, SIGTERM was caught by the shell but never propagated to the actual hook process. The shell exited; the hook kept running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;timedOut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGTERM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SIGKILL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* gone */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;500ms grace, then a hard kill. Slightly inelegant. Actually works.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Malformed regex in &lt;code&gt;match&lt;/code&gt; used to fire on every tool
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;new RegExp("[unclosed")&lt;/code&gt; throws. The first version caught the throw and returned &lt;code&gt;true&lt;/code&gt; — assuming the user's intent was permissive. That's wrong: a typo in a regex shouldn't suddenly cause a &lt;code&gt;PreToolUse&lt;/code&gt; hook to fire on every tool call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// malformed regex → don't fire (safer than firing on every tool)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typo in &lt;code&gt;match&lt;/code&gt; now makes the hook silently inactive (visible in &lt;code&gt;/hooks list&lt;/code&gt;) instead of suddenly gating every tool call.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A typo in settings.json crashed the entire CLI
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;JSON.parse&lt;/code&gt; throws on malformed JSON. The first version let it propagate. One missing comma in &lt;code&gt;~/.reasonix/settings.json&lt;/code&gt; and &lt;code&gt;reasonix chat&lt;/code&gt; exited with a stack trace before showing the TUI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readSettingsFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;HookSettings&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HookSettings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* malformed JSON → treat as no hooks; don't lose the whole CLI to a typo */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The principle: a tool that's broken should still be openable, even if degraded. Configuration is the most fragile part of any system; it should never take down the part that lets you fix the configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still missing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No structured rewrite from hooks.&lt;/strong&gt; A &lt;code&gt;PreToolUse&lt;/code&gt; hook can block or pass; it can't rewrite arguments. Want to redact secrets from &lt;code&gt;toolArgs.path&lt;/code&gt;? You can't — only refuse the call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hook timing in &lt;code&gt;/stats&lt;/code&gt;.&lt;/strong&gt; Hook duration is captured per-outcome (&lt;code&gt;durationMs&lt;/code&gt;) but isn't aggregated. A 30-second formatter hook on every edit is a real productivity tax — should be visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;match&lt;/code&gt; for non-tool events.&lt;/strong&gt; &lt;code&gt;UserPromptSubmit&lt;/code&gt; always fires for every prompt. There's an argument for &lt;code&gt;match&lt;/code&gt; working as a regex over the prompt text. Haven't been convinced yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composition with skills.&lt;/strong&gt; Reasonix 0.4.26 added subagents-via-skills. Subagent invocations don't currently fire &lt;code&gt;PreToolUse&lt;/code&gt; (a subagent isn't a tool in the model's eyes). Should they? Probably yes, separate event: &lt;code&gt;PreSubagent&lt;/code&gt;. On the roadmap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Open questions I'd love feedback on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anchored vs substring &lt;code&gt;match&lt;/code&gt;.&lt;/strong&gt; Strong opinion, weakly held. Substring crowd has a point about ergonomics. Anchored crowd has a point about predictability. Vote in the comments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Should hooks be allowed to mutate the payload?&lt;/strong&gt; I deliberately said no. But "redact this argument before the tool runs" is real. Worth a &lt;code&gt;parseStdoutAsJson: true&lt;/code&gt; flag?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-event vs per-tool default timeouts.&lt;/strong&gt; I picked 5s for gating, 30s for observing — uniform across tools. But a &lt;code&gt;PostToolUse&lt;/code&gt; hook on &lt;code&gt;web_search&lt;/code&gt; (already 10s of latency) is different from one on &lt;code&gt;edit_file&lt;/code&gt; (instant). Should defaults adapt?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; reasonix
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .reasonix
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .reasonix/settings.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "hooks": {
    "Stop": [
      { "command": "echo 'turn done.' &amp;amp;&amp;amp; date" }
    ]
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;reasonix chat
&lt;span class="c"&gt;# /hooks         list active&lt;/span&gt;
&lt;span class="c"&gt;# /hooks reload  re-read after editing settings.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full source: &lt;a href="https://github.com/esengine/reasonix" rel="noopener noreferrer"&gt;github.com/esengine/reasonix&lt;/a&gt;, specifically &lt;a href="https://github.com/esengine/reasonix/blob/main/src/hooks.ts" rel="noopener noreferrer"&gt;src/hooks.ts&lt;/a&gt; and &lt;a href="https://github.com/esengine/reasonix/blob/main/tests/hooks.test.ts" rel="noopener noreferrer"&gt;tests/hooks.test.ts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want the bigger picture on Reasonix (cache-first loop, R1 thought harvesting, branching), the &lt;a href="https://dev.to/esengine/how-a-deepseek-only-agent-framework-hit-85-prefix-cache-rate-and-saved-93-vs-claude-5c9g"&gt;first article&lt;/a&gt; covers that.&lt;/p&gt;

&lt;p&gt;Issues, design arguments, and counter-examples especially welcome.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>tutorial</category>
      <category>cli</category>
    </item>
    <item>
      <title>How a DeepSeek-only agent framework hit 85% prefix cache rate (and saved 93% vs Claude)</title>
      <dc:creator>YHH</dc:creator>
      <pubDate>Tue, 21 Apr 2026 11:54:44 +0000</pubDate>
      <link>https://dev.to/esengine/how-a-deepseek-only-agent-framework-hit-85-prefix-cache-rate-and-saved-93-vs-claude-5c9g</link>
      <guid>https://dev.to/esengine/how-a-deepseek-only-agent-framework-hit-85-prefix-cache-rate-and-saved-93-vs-claude-5c9g</guid>
      <description>&lt;p&gt;I've been running DeepSeek behind LangChain for a few months for a side project. Worked fine, except one day I noticed&lt;br&gt;
  something weird: DeepSeek's pricing page advertises &lt;strong&gt;cached input tokens at ~10% of the miss rate&lt;/strong&gt;, but my bills didn't&lt;br&gt;
  reflect that at all.&lt;/p&gt;

&lt;p&gt;I dug in. The cache is byte-prefix based. The moment your request's prefix differs from the previous one by even a single&lt;br&gt;
  character, you pay full price. And LangChain — along with every generic agent framework I checked — rebuilds the prompt&lt;br&gt;
  every turn. Timestamps get injected. History gets reordered. Tool schemas re-serialize with different whitespace. The prefix&lt;br&gt;
   drifts, the cache never hits.&lt;/p&gt;

&lt;p&gt;So I wrote something opinionated: &lt;strong&gt;Reasonix&lt;/strong&gt; — a TypeScript agent framework built &lt;strong&gt;only&lt;/strong&gt; for DeepSeek. No multi-provider&lt;br&gt;
   abstraction, no orchestration graph, no RAG. Just three things done deeply.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📦 &lt;code&gt;npm install -g reasonix &amp;amp;&amp;amp; reasonix chat&lt;/code&gt;&lt;br&gt;
🔗 GitHub: &lt;a href="https://github.com/esengine/reasonix" rel="noopener noreferrer"&gt;esengine/reasonix&lt;/a&gt;&lt;br&gt;
📜 MIT License&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;## The numbers up front&lt;/p&gt;

&lt;p&gt;Measured against the live DeepSeek API, not marketing math:&lt;/p&gt;

&lt;p&gt;| Scenario | Model | Turns | Cache hit | Cost | Same on Claude Sonnet 4.6 | Savings |&lt;br&gt;
  |---|---|---|---|---|---|---|&lt;br&gt;
  | Multi-turn chat | &lt;code&gt;deepseek-chat&lt;/code&gt; | 5 | &lt;strong&gt;85.2%&lt;/strong&gt; | $0.000923 | $0.015174 | &lt;strong&gt;93.9%&lt;/strong&gt; |&lt;br&gt;
  | Tool-use (calculator) | &lt;code&gt;deepseek-chat&lt;/code&gt; | 2 | &lt;strong&gt;94.9%&lt;/strong&gt; | $0.000142 | $0.003351 | &lt;strong&gt;95.8%&lt;/strong&gt; |&lt;br&gt;
  | R1 reasoning + harvest | &lt;code&gt;deepseek-reasoner&lt;/code&gt; | 1 | 72.7% | $0.006478 | $0.044484 | 85.4% |&lt;/p&gt;

&lt;p&gt;Numbers come straight from &lt;code&gt;usage.prompt_cache_hit_tokens&lt;/code&gt; on real API responses. You can install Reasonix and verify in 2&lt;br&gt;
  minutes.&lt;/p&gt;

&lt;p&gt;## Pillar 1 — Cache-First Loop&lt;/p&gt;

&lt;p&gt;The problem again: DeepSeek's cache only fires on identical byte prefix. Generic frameworks rebuild prompts, so the prefix&lt;br&gt;
  drifts, so the cache rarely hits.&lt;/p&gt;

&lt;p&gt;The fix is structural. Every request's context gets partitioned into three regions with strict invariants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ┌─────────────────────────────────────┐
  │ IMMUTABLE PREFIX                    │ ← frozen at session start
  │   system + tool_specs + few_shots   │   this is the cache target
  ├─────────────────────────────────────┤
  │ APPEND-ONLY LOG                     │ ← grows monotonically
  │   [user₁][assistant₁][tool₁]...     │   prior turns preserve as prefix
  ├─────────────────────────────────────┤
  │ VOLATILE SCRATCH                    │ ← reset each turn
  │   R1 thoughts, transient state      │   never sent upstream
  └─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In code, the prefix is hashed at construction and pinned. The log's &lt;code&gt;append()&lt;/code&gt; method refuses any mutation. The scratch gets&lt;br&gt;
   wiped at every turn boundary.&lt;/p&gt;

&lt;p&gt;That's it. That single discipline is enough to push cache hit rates to 85-95% on real sessions. Nothing else in the&lt;br&gt;
  framework would matter if this was wrong.&lt;/p&gt;

&lt;p&gt;## Pillar 2 — R1 Thought Harvesting&lt;/p&gt;

&lt;p&gt;DeepSeek's reasoning model &lt;code&gt;deepseek-reasoner&lt;/code&gt; (aka R1) emits extensive &lt;code&gt;reasoning_content&lt;/code&gt; — often 1000+ tokens of&lt;br&gt;
  step-by-step thinking. DeepSeek's own docs recommend &lt;strong&gt;not&lt;/strong&gt; feeding it back to the next turn (it hurts quality). So most&lt;br&gt;
  frameworks just display it or drop it.&lt;/p&gt;

&lt;p&gt;That's leaving a plan on the table. R1's reasoning trace is literally the model thinking out loud about subgoals,&lt;br&gt;
  hypotheses, and uncertainties. I pipe it through a cheap secondary V3 call in JSON mode and extract structured state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TypedPlanState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;subgoals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;      &lt;span class="c1"&gt;// concrete intermediate objectives&lt;/span&gt;
    &lt;span class="nl"&gt;hypotheses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;    &lt;span class="c1"&gt;// candidate approaches being weighed&lt;/span&gt;
    &lt;span class="nl"&gt;uncertainties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="c1"&gt;// things R1 flags as unclear&lt;/span&gt;
    &lt;span class="nl"&gt;rejectedPaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="c1"&gt;// approaches considered and abandoned&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's R1 on a classic logic puzzle — "3 boxes with swapped labels; pick one fruit to determine all three contents":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ‹ subgoals (3): enumerate label-content permutations · decide which box to sample · verify uniqueness
  ‹ hypotheses (3): sample from "apple" box · sample from "orange" box · sample from "mixed" box
  ‹ uncertainties (2): can a single pick uniquely determine all? · does "mixed" contain equal ratios?
  ‹ rejected (2): sampling from "apple" box (ambiguous) · sampling from "orange" box (symmetric)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every field maps to actual content in R1's reasoning trace. V3 is cheap enough (~$0.0001/turn) that this is essentially&lt;br&gt;
  free. Opt-in via &lt;code&gt;reasonix chat --harvest&lt;/code&gt; or &lt;code&gt;/harvest on&lt;/code&gt; inside the TUI.&lt;/p&gt;

&lt;p&gt;## Pillar 3 — Tool-Call Repair&lt;/p&gt;

&lt;p&gt;DeepSeek has several known tool-use quirks that generic frameworks don't handle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deep or wide schemas drop arguments.&lt;/strong&gt; Tool schemas with more than ~10 leaf parameters or more than 2 levels of nesting
cause V3/R1 to silently omit fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R1 leaks tool calls into &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt;.&lt;/strong&gt; The model writes tool-call JSON inside its reasoning trace and forgets to surface
it in the actual &lt;code&gt;tool_calls&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON gets truncated.&lt;/strong&gt; Long &lt;code&gt;arguments&lt;/code&gt; payloads hit &lt;code&gt;max_tokens&lt;/code&gt; mid-structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call storms.&lt;/strong&gt; The model hammers the same tool with identical arguments in an infinite loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reasonix's repair layer has four passes running on every turn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// 1. Auto-flatten deep/wide schemas&lt;/span&gt;
  &lt;span class="nx"&gt;ToolRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updateProfile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="p"&gt;}},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;updateInDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Internally shown to the model as a flat schema:&lt;/span&gt;
  &lt;span class="c1"&gt;//   {"user.profile.name": "...", "user.profile.age": ...}&lt;/span&gt;
  &lt;span class="c1"&gt;// On dispatch, args re-nested back to { user: { profile: { ... } } }&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Scavenge: regex + JSON parser sweeps reasoning_content for missed calls&lt;/span&gt;
  &lt;span class="c1"&gt;// 3. Truncation recovery: close braces, trim trailing commas, fill dangling keys&lt;/span&gt;
  &lt;span class="c1"&gt;// 4. Storm breaker: sliding-window dedup of (tool, args) tuples&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four are always on. No user configuration.&lt;/p&gt;

&lt;p&gt;## Bonus: Self-Consistency Branching&lt;/p&gt;

&lt;p&gt;Here's the fun one. DeepSeek is roughly 20× cheaper than Claude Sonnet 4.6. That means &lt;strong&gt;three parallel R1 samples per turn&lt;br&gt;
  is still cheaper than a single Claude call&lt;/strong&gt;. What was a research luxury (self-consistency sampling) becomes a practical&lt;br&gt;
  default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  reasonix chat &lt;span class="nt"&gt;--branch&lt;/span&gt; 3
  &lt;span class="c"&gt;# or inside the TUI:&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /preset max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three samples fire in parallel at temperatures 0.0 / 0.5 / 1.0. Each one's reasoning is harvested. The default selector&lt;br&gt;
  picks whichever sample has the fewest flagged &lt;code&gt;uncertainties&lt;/code&gt; (tie-break on shorter answer length — Occam's razor as a&lt;br&gt;
  heuristic).&lt;/p&gt;

&lt;p&gt;TUI shows this live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  🔀 branched 3 samples → picked #1   #0 T=0.0 u=2   ▸#1 T=0.5 u=0   #2 T=1.0 u=3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anecdotally it lifts accuracy 10-15 percentage points on medium-difficulty reasoning, at roughly 1/5 the cost of a single&lt;br&gt;
  Claude pass. I haven't run a formal benchmark yet — that's next.&lt;/p&gt;

&lt;p&gt;## What it's explicitly not&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not a LangChain replacement. No multi-provider, no graph orchestration, no RAG.&lt;/li&gt;
&lt;li&gt;Not a drop-in for OpenAI-compatible code. The whole point is DeepSeek-specific.&lt;/li&gt;
&lt;li&gt;Not production-ready. v0.0.6 pre-alpha, 135 passing tests, no formal benchmarks yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;## Quick start&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; reasonix
  reasonix chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First launch prompts for your DeepSeek API key and saves it to &lt;code&gt;~/.reasonix/config.json&lt;/code&gt;. Sessions auto-persist, so chat 2&lt;br&gt;
  hours of work, quit, come back tomorrow, type &lt;code&gt;reasonix chat&lt;/code&gt; — you're back where you left off.&lt;/p&gt;

&lt;p&gt;Inside the TUI, slash commands cover everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;  /preset fast|smart|max    one-tap config (fast = default)
&lt;/span&gt;&lt;span class="gp"&gt;  /model &amp;lt;id&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;deepseek-chat or deepseek-reasoner
&lt;span class="go"&gt;  /harvest [on|off]         Pillar 2 toggle
&lt;/span&gt;&lt;span class="gp"&gt;  /branch &amp;lt;N|off&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;N parallel samples &lt;span class="o"&gt;(&amp;gt;=&lt;/span&gt;2&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;  /sessions                 list saved sessions
  /forget                   delete current session
  /help                     full list
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No flag-soup to memorize. A command strip under the prompt shows the top-level commands at all times.&lt;/p&gt;

&lt;p&gt;## Library usage&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;CacheFirstLoop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;DeepSeekClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;ImmutablePrefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;ToolRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reasonix&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeepSeekClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// reads DEEPSEEK_API_KEY&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolRegistry&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CacheFirstLoop&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImmutablePrefix&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are a math helper.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;toolSpecs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;specs&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;harvest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;math-tutor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What is 17 + 25?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant_final&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="c1"&gt;// { turns: 2, totalCostUsd: 0.0003, savingsVsClaudePct: 94, cacheHitRatio: 0.87 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;## Open questions I'd love feedback on&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Branching selector heuristic.&lt;/strong&gt; The default is &lt;code&gt;min(uncertainties.length)&lt;/code&gt; with length tie-break. That's obviously&lt;br&gt;
naive. What signals would you combine? Cross-sample answer similarity? Tool-call success rate per sample? An LLM-judge pass?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Harvest cost/value trade-off.&lt;/strong&gt; The $0.0001/turn V3 call feels negligible but it's a floor on per-turn cost. Has anyone&lt;br&gt;
tried fine-tuning R1 to output structured plan state directly?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache continuity across config changes.&lt;/strong&gt; Right now changing the system prompt mid-session invalidates the prefix&lt;br&gt;
cache. Is there a migration path that preserves the existing log's value?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Full source: &lt;a href="https://github.com/esengine/reasonix" rel="noopener noreferrer"&gt;github.com/esengine/reasonix&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install: &lt;code&gt;npm install -g reasonix&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Issues, PRs, and benchmarks especially welcome.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ai</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
