<?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: Madhur Mittal</title>
    <description>The latest articles on DEV Community by Madhur Mittal (@maddy30445r).</description>
    <link>https://dev.to/maddy30445r</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4130349%2F7b5c258a-8e27-4245-acad-35c0134006b3.png</url>
      <title>DEV Community: Madhur Mittal</title>
      <link>https://dev.to/maddy30445r</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maddy30445r"/>
    <language>en</language>
    <item>
      <title>Claude Code Stop hooks: making the agent finish its homework"</title>
      <dc:creator>Madhur Mittal</dc:creator>
      <pubDate>Wed, 07 Oct 2026 10:21:34 +0000</pubDate>
      <link>https://dev.to/maddy30445r/claude-code-stop-hooks-making-the-agent-finish-its-homework-hmd</link>
      <guid>https://dev.to/maddy30445r/claude-code-stop-hooks-making-the-agent-finish-its-homework-hmd</guid>
      <description>&lt;p&gt;Every coding agent I use has the same habit. It does the work, writes a lovely summary, and stops. The summary lives in the chat. The chat scrolls away. The &lt;code&gt;NOTES.md&lt;/code&gt; I asked it to keep up to date hasn't been touched since Tuesday.&lt;/p&gt;

&lt;p&gt;You can ask nicely in &lt;code&gt;CLAUDE.md&lt;/code&gt;, and it will mostly listen. "Mostly" is doing a lot of work in that sentence.&lt;/p&gt;

&lt;p&gt;Claude Code has a firmer tool for this: a Stop hook, a script that runs when the agent is about to finish and is allowed to say "not yet". I shipped one in Deiko this week, then shipped a fix for it the same day. The fix taught me the one rule I'd now give anyone writing a Stop hook, so that's where this post is heading. Basics first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Stop hook is, and when it fires
&lt;/h2&gt;

&lt;p&gt;A Stop hook runs when the main agent has finished responding. It doesn't run when you interrupt with Esc. An API error fires a separate &lt;code&gt;StopFailure&lt;/code&gt; event, and subagents get their own &lt;code&gt;SubagentStop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Hooks live in &lt;code&gt;settings.json&lt;/code&gt;: &lt;code&gt;~/.claude/settings.json&lt;/code&gt; for all your projects, &lt;code&gt;.claude/settings.json&lt;/code&gt; for one repo. There are three levels: the event, a group, and the handlers inside the group. Stop has no matcher (if you add one, it's silently ignored), so the group is just a wrapper:&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;"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;"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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&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;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/notes-guard.mjs"&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;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;Setting &lt;code&gt;args&lt;/code&gt; runs the command directly, without a shell, which is what the &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks reference&lt;/a&gt; recommends whenever you use a path placeholder like &lt;code&gt;${CLAUDE_PROJECT_DIR}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The script gets a JSON object on stdin. For Stop, the parts you'll use look like this:&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;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transcript_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;"~/.claude/projects/.../00893aaf-....jsonl"&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/you/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;"hook_event_name"&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"&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_hook_active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_assistant_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I've finished the refactor. Here's what changed..."&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;There are a few more fields (&lt;code&gt;permission_mode&lt;/code&gt;, &lt;code&gt;background_tasks&lt;/code&gt;, &lt;code&gt;session_crons&lt;/code&gt;), but these are the ones that matter here.&lt;/p&gt;

&lt;h2&gt;
  
  
  How "block" keeps the agent going
&lt;/h2&gt;

&lt;p&gt;Print nothing and exit 0, and the agent stops as normal. To keep it working, exit 0 and print this on stdout:&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;"decision"&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"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Run the tests before you finish."&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;&lt;code&gt;reason&lt;/code&gt; is required when you block, and it's what Claude reads as its next instruction. You can also block by exiting 2, in which case your stderr becomes the reason. Pick one style per hook.&lt;/p&gt;

&lt;p&gt;The trap here is exit 1. Every Unix instinct says 1 means "no", but for a Stop hook it's a non-blocking error: you get a "hook error" notice and the agent stops anyway. Same if your script path is mistyped. A guard with a typo in &lt;code&gt;settings.json&lt;/code&gt; doesn't guard anything, so watch the first run.&lt;/p&gt;

&lt;p&gt;Also: stdout has to be only the JSON. A shell profile that prints a greeting can land in front of it, and then your block is quietly ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Stop hook example you can copy
&lt;/h2&gt;

&lt;p&gt;Here's one I'd actually use: don't finish a turn without touching &lt;code&gt;NOTES.md&lt;/code&gt;. The interesting part is "a turn". The hook needs to know when this turn started, and Claude Code gives you a documented way to find out: a &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook fires the moment you send a prompt. So one script handles both events. On a prompt, it writes down the time. On Stop, it checks whether &lt;code&gt;NOTES.md&lt;/code&gt; changed since then.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;&lt;span class="c1"&gt;// .claude/hooks/notes-guard.mjs: runs on UserPromptSubmit and Stop.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;statSync&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;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&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;node:path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tmpdir&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;node:os&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&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="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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;mark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tmpdir&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;`notes-guard-&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;session_id&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;if &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;hook_event_name&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt; &lt;span class="c1"&gt;// when this turn began&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hook_event_name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stop&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="o"&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;stop_hook_active&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;began&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&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;mark&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;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLAUDE_PROJECT_DIR&lt;/span&gt; &lt;span class="o"&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;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NOTES.md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;edited&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;edited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;statSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;mtimeMs&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edited&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;began&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;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;decision&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="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Before you stop: add a line to NOTES.md saying what you changed this turn and why.&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;// anything odd: let the agent stop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the same command under both events:&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;"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="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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&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;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/notes-guard.mjs"&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;"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="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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&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;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"${CLAUDE_PROJECT_DIR}/.claude/hooks/notes-guard.mjs"&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;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;It's strict: every turn has to end with a note, including "what does this function do?" turns. That's fine for a long autonomous session and annoying for a chatty one. Swap the condition for whatever "done" means to you: a test run's exit code, a changelog line, a file under &lt;code&gt;docs/&lt;/code&gt;. Keep the shape.&lt;/p&gt;

&lt;p&gt;If you don't need a script at all, Claude Code also supports &lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;prompt and agent hooks&lt;/a&gt; on Stop, where a model decides whether the work is finished, and a &lt;a href="https://code.claude.com/docs/en/goal" rel="noopener noreferrer"&gt;&lt;code&gt;/goal&lt;/code&gt; command&lt;/a&gt; that does the same for one session. For a fuzzy condition like "are all the tasks done?", that's the better pick. For anything you can check with a file timestamp or an exit code, a script is cheaper and gives the same answer every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infinite-loop trap
&lt;/h2&gt;

&lt;p&gt;Write a Stop hook that blocks whenever a condition isn't met, and sooner or later the condition can't be met. The tests need a database that isn't running. The agent has no write access to &lt;code&gt;NOTES.md&lt;/code&gt;. Your hook blocks, the agent tries, fails, stops, your hook blocks again.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;stop_hook_active&lt;/code&gt; is for. It's &lt;code&gt;true&lt;/code&gt; when Claude is already continuing because a Stop hook blocked. Check it first, and let the agent go: you asked once, it tried, move on. Claude Code also has a backstop: after eight blocks in a row it overrides the hook and ends the turn (&lt;code&gt;CLAUDE_CODE_STOP_HOOK_BLOCK_CAP&lt;/code&gt; changes the number). Eight rounds of an agent being nagged is still eight rounds of tokens, though. Check the flag.&lt;/p&gt;

&lt;p&gt;The example above checks it in the &lt;code&gt;if&lt;/code&gt;, and everything else lives inside a &lt;code&gt;try&lt;/code&gt; that falls through to "let it stop". A missing timestamp file, a broken stdin, an unreadable path: all of them end the turn normally. A guard that fails closed turns one bad file into an agent that can't finish anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real one: Deiko's report-back hook
&lt;/h2&gt;

&lt;p&gt;Quick context if you're new here (or start with &lt;a href="https://deiko.app/blog/claude-code-memory-between-sessions" rel="noopener noreferrer"&gt;giving Claude Code a memory between sessions&lt;/a&gt;): &lt;a href="https://deiko.app/" rel="noopener noreferrer"&gt;Deiko&lt;/a&gt; is a Mac app where you point at things on screen and talk, and it turns that into a brief for your coding agent. Every brief is filed into a task, and each task keeps a note: where it stands, what was decided, what agents reported back. That last part only works if the agent actually reports back, through a &lt;code&gt;save_outcome&lt;/code&gt; tool on Deiko's local memory server or by writing an &lt;code&gt;outcome.md&lt;/code&gt; file the brief names. The &lt;a href="https://deiko.app/blog/context-memory-for-ai-coding-agents" rel="noopener noreferrer"&gt;post on building memory&lt;/a&gt; has the long version.&lt;/p&gt;

&lt;p&gt;Agents forget. So in 0.5.8, connecting memory in Settings also adds a Stop hook to Claude Code. The whole thing is &lt;a href="https://github.com/maddy30445r/deiko/blob/main/packages/core/src/claude-stop-hook.mjs" rel="noopener noreferrer"&gt;about 70 lines of Node with no dependencies&lt;/a&gt;, and it makes four decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Only act on a Deiko brief.&lt;/strong&gt; Every brief contains one fixed sentence asking for a report, which names the brief's id and the path of its &lt;code&gt;outcome.md&lt;/code&gt;. The hook looks for that sentence with a regular expression. No sentence, no opinion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the file, not the conversation.&lt;/strong&gt; &lt;code&gt;save_outcome&lt;/code&gt; writes the same &lt;code&gt;outcome.md&lt;/code&gt; the brief points at, so one check covers both routes: was that file modified after the brief arrived? The arrival time comes from the brief's timestamp in the transcript; the other side is the file's mtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block once.&lt;/strong&gt; If there's no report, it blocks with a reason naming the brief and both ways to save. If &lt;code&gt;stop_hook_active&lt;/code&gt; is true, it does nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never stand in the agent's way.&lt;/strong&gt; Bad JSON, a missing transcript, an unreadable file: the hook prints nothing and exits 0. That comment is in the code, word for word.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The decision is one function that returns either &lt;code&gt;null&lt;/code&gt; or the block object, with file access passed in so tests can fake it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;decide&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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="nx"&gt;mtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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;statSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;mtimeMs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&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;stop_hook_active&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&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;transcript_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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;transcript&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;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read&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;transcript_path&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;null&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;brief&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;latestBrief&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript&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;brief&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;saved&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brief&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcome&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;/* not written yet */&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;saved&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;brief&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&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="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Before you finish: save what you did for Deiko brief &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;brief&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installing it is the boring part, and I wanted it boring. The Mac app reads &lt;code&gt;~/.claude/settings.json&lt;/code&gt; and refuses to touch it if it won't parse. It finds its own entry by the script's file name, so a moved app replaces its old entry instead of adding a second, and your other hooks stay exactly where they were. It keeps one backup, writes through a temp file and a rename, then reads the file back to confirm. Disconnecting removes only Deiko's entry. &lt;a href="https://github.com/maddy30445r/deiko/blob/main/apps/macos/Sources/DeikoHandoff/ClaudeStopHook.swift" rel="noopener noreferrer"&gt;That code is here&lt;/a&gt; if you're writing an installer of your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: homework that was never set
&lt;/h2&gt;

&lt;p&gt;Version one looked for the latest brief &lt;em&gt;anywhere&lt;/em&gt; in the transcript.&lt;/p&gt;

&lt;p&gt;That sounds right until you have a long working session. I'd pasted a brief into Claude Code early on, not to work on it but to talk about it. We talked, then moved on to other things. Lots of other things.&lt;/p&gt;

&lt;p&gt;At the end of every single turn after that, the hook asked for a report on that brief.&lt;/p&gt;

&lt;p&gt;Each turn it blocked only once, so nothing looped. But &lt;code&gt;stop_hook_active&lt;/code&gt; resets with every new prompt, and the brief was still sitting in the transcript with no outcome written after it. So: block, every turn, forever. The best case is an agent wasting a turn explaining it wasn't working on that. The worst case is an agent that obligingly invents a report for work it never did and files it into that task's memory.&lt;/p&gt;

&lt;p&gt;The fix shipped in &lt;a href="https://deiko.app/changelog" rel="noopener noreferrer"&gt;0.5.10&lt;/a&gt;, on 30 September. Here's the heart of the &lt;a href="https://github.com/maddy30445r/deiko/commit/bbf6ad3" rel="noopener noreferrer"&gt;diff&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 0.5.8: remember the last brief seen anywhere&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;MARKER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="k"&gt;if &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="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;outcome&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&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="c1"&gt;// 0.5.10: every real prompt resets it, so only the latest one counts&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isMeta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="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;body&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Stop hook feedback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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;MARKER&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;outcome&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new rule: the hook only cares about the latest thing &lt;em&gt;you&lt;/em&gt; typed. If that's a brief, check for a report. If it's anything else, the brief isn't this turn's work and the hook stays out of it.&lt;/p&gt;

&lt;p&gt;The skips matter as much as the rule. In Claude Code's transcript, a lot of entries are marked as coming from the user that you never typed: tool results, meta entries like system reminders, and the hook's own feedback. Once "latest wins", every one of those would wipe out the brief. A tool result arriving mid-turn would make the hook think you'd moved on. So tool results drop out (only text content counts), meta entries drop out, and anything starting with "Stop hook feedback" drops out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope it to the current turn
&lt;/h2&gt;

&lt;p&gt;Here's the general version, for any Stop hook: &lt;strong&gt;a Stop hook should judge this turn, not the whole session.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Were the tests run?" means since the last prompt, not ever. "Was NOTES.md updated?" means since this turn started, not since Tuesday. Whatever condition you check, anchor it to the moment the current prompt arrived, and make sure the anchor is a prompt a person actually sent.&lt;/p&gt;

&lt;p&gt;There are two ways to find that moment. The documented one is a &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook that records it, like the example above. It also receives the prompt text, so you can decide right there whether this turn is one you care about. Deiko's hook reads the transcript instead, which keeps it to a single entry in &lt;code&gt;settings.json&lt;/code&gt;. The cost is depending on the transcript's format, which isn't a published API, and the docs warn the file can lag behind the conversation. The prompt that started the turn is written early, so it's there by the time Stop fires. I'd still parse it defensively, which is why every bad line is skipped rather than fatal.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to test a Stop hook
&lt;/h2&gt;

&lt;p&gt;Keep the decision in a pure function and the stdin plumbing in a few lines at the bottom. Then you can test the function with a fake transcript, built in the test as a few JSON lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&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 brief pasted earlier and then talked about does not nag later turns&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="o"&gt;=&amp;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;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;entries&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;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&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;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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;latestBrief&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brief1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;later&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;latestBrief&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brief1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20260929-100000&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;p&gt;Deiko's &lt;a href="https://github.com/maddy30445r/deiko/blob/main/packages/core/test/claude-stop-hook.test.mjs" rel="noopener noreferrer"&gt;six tests&lt;/a&gt; use &lt;code&gt;node:test&lt;/code&gt; and nothing else. They cover blocking once and never twice, a fresh report against a stale one, a chat with no brief, garbage on stdin, and the bug above. Write the bug's test before the fix. Watching it go red is oddly satisfying.&lt;/p&gt;

&lt;p&gt;Then do one end-to-end check with the real CLI, because a perfect function behind a mistyped path is a disabled hook. In a scratch folder, save the settings from the example as &lt;code&gt;hook-settings.json&lt;/code&gt; and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Add a comment to the top of app.js"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--settings&lt;/span&gt; hook-settings.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; acceptEdits &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--debug-file&lt;/span&gt; hook.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-p&lt;/code&gt; runs one prompt without the interactive UI, &lt;code&gt;--settings&lt;/code&gt; loads your hooks for just this run, &lt;code&gt;acceptEdits&lt;/code&gt; lets the agent write files without asking, and the debug log records which hooks matched, their exit codes and their output. Pass if &lt;code&gt;NOTES.md&lt;/code&gt; exists afterwards and &lt;code&gt;hook.log&lt;/code&gt; shows your Stop hook ran. All four flags are in the &lt;a href="https://code.claude.com/docs/en/cli-reference" rel="noopener noreferrer"&gt;CLI reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've written a Stop hook that went wrong in an interesting way, I'd like to hear it. I'm &lt;a href="https://x.com/Deiko_App" rel="noopener noreferrer"&gt;@Deiko_App&lt;/a&gt; on X.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop hook not working? The usual causes
&lt;/h2&gt;

&lt;p&gt;Most broken Stop hooks fail in one of five ways. Start Claude Code with &lt;code&gt;claude --debug&lt;/code&gt; and the log at &lt;code&gt;~/.claude/debug/&amp;lt;session-id&amp;gt;.txt&lt;/code&gt; shows what each hook printed and how it was read.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It never fires.&lt;/strong&gt; Type &lt;code&gt;/hooks&lt;/code&gt; and check it’s listed under Stop. If it isn’t, the settings file is in the wrong place (&lt;code&gt;.claude/settings.json&lt;/code&gt; for a project, &lt;code&gt;~/.claude/settings.json&lt;/code&gt; for everything) or isn’t valid JSON: no trailing commas, no comments. Stop takes no matcher, so a matcher won’t help. It also doesn’t fire when you interrupt Claude, and an API error fires &lt;code&gt;StopFailure&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Stop hook error” with a JSON validation message.&lt;/strong&gt; Your output parsed as JSON but doesn’t fit the schema: a field at the wrong level, or a &lt;code&gt;decision&lt;/code&gt; other than &lt;code&gt;"block"&lt;/code&gt;. To let the agent stop, print nothing at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Stop hook error” with a parse message.&lt;/strong&gt; The output started with &lt;code&gt;{&lt;/code&gt; but wasn’t valid JSON, usually a quote inside the reason. Build it with &lt;code&gt;JSON.stringify&lt;/code&gt; or &lt;code&gt;jq&lt;/code&gt;, never by gluing strings together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The JSON is ignored, with no error.&lt;/strong&gt; Something printed before it, often an &lt;code&gt;echo&lt;/code&gt; in your shell profile, so stdout no longer starts with &lt;code&gt;{&lt;/code&gt; and Claude Code reads it as plain text. Wrap that echo in an interactive-shell check, or run the hook in exec form with &lt;code&gt;"args": []&lt;/code&gt; so no shell is involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It keeps blocking, then gives up.&lt;/strong&gt; Claude Code overrides a Stop hook after eight blocks in a row and ends the turn with a warning. Read &lt;code&gt;stop_hook_active&lt;/code&gt; and let the agent go when it’s true. And if your hook blocks on every turn, check that it’s judging the current one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this is in the official &lt;a href="https://code.claude.com/docs/en/hooks-guide#limitations-and-troubleshooting" rel="noopener noreferrer"&gt;hooks troubleshooting guide&lt;/a&gt;, which is worth a bookmark.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; Print &lt;code&gt;{"decision":"block","reason":"..."}&lt;/code&gt; to keep the agent going. Check &lt;code&gt;stop_hook_active&lt;/code&gt; so you ask once. Let every failure end the turn. And judge only the current turn: anchor your check to the latest prompt a person actually sent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Questions people ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I stop a Claude Code Stop hook from looping forever?
&lt;/h3&gt;

&lt;p&gt;Read stop_hook_active from the JSON on stdin and let the agent stop when it is true. It is true when Claude is already continuing because a Stop hook blocked. Claude Code also overrides a Stop hook after eight blocks in a row, but you don't want to rely on that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should a Stop hook exit 2 or print JSON?
&lt;/h3&gt;

&lt;p&gt;Either blocks the stop. Exit 2 uses your stderr as the reason. Exit 0 with {"decision": "block", "reason": "..."} on stdout gives you structured control. Pick one per hook. Exit 1 does not block: Claude Code treats it as a non-blocking error and the agent stops.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a Stop hook run when I press Esc?
&lt;/h3&gt;

&lt;p&gt;No. Stop runs when the main agent finishes responding, not when you interrupt it. API errors fire a separate StopFailure event, and subagents fire SubagentStop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a script, or can a prompt decide?
&lt;/h3&gt;

&lt;p&gt;Claude Code also supports prompt and agent hooks on Stop, where a model decides whether the work is done, and a /goal command for one session. Use a command script when the check is deterministic, like a file's modification time or a test exit code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my Claude Code Stop hook not working?
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;/hooks&lt;/code&gt; to check it is registered under Stop, make sure the settings file is valid JSON in the right place, and start Claude Code with &lt;code&gt;claude --debug&lt;/code&gt; to see what the hook printed. The most common silent cause is extra output, such as a shell-profile echo, before the JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does “Stop hook error: JSON validation failed” mean?
&lt;/h3&gt;

&lt;p&gt;Your hook printed a JSON object that does not match the Stop hook schema, such as a misplaced field or a decision other than "block". The agent stops anyway. To block, print {"decision":"block","reason":"..."}; to let it stop, print nothing.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I open-sourced my Mac app for talking to coding agents. Here's how it works</title>
      <dc:creator>Madhur Mittal</dc:creator>
      <pubDate>Wed, 30 Sep 2026 18:44:56 +0000</pubDate>
      <link>https://dev.to/maddy30445r/i-open-sourced-my-mac-app-for-talking-to-coding-agents-heres-how-it-works-4ded</link>
      <guid>https://dev.to/maddy30445r/i-open-sourced-my-mac-app-for-talking-to-coding-agents-heres-how-it-works-4ded</guid>
      <description>&lt;p&gt;I've spent two months building Deiko, a Mac app for talking to coding agents, and today it's open source under MIT. Here's what it does and how the interesting parts work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two problems
&lt;/h2&gt;

&lt;p&gt;Coding agents can't see what you're looking at. So you type a paragraph describing a screen they never saw, take a screenshot, attach it, then explain which part of it you meant.&lt;/p&gt;

&lt;p&gt;And they forget. Every new chat with Claude Code, Cursor or Codex starts from zero, so on Friday you explain Tuesday's bug again, including the fix that already didn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Showing instead of typing
&lt;/h2&gt;

&lt;p&gt;Double-tap Right Option and talk. While you talk, you point at things with the cursor, or hold Left Option and draw around a region. Deiko records your voice, crops the screen around each point, reads the accessibility label under the cursor (a button's title, a cell's value) and notes the window and page titles.&lt;/p&gt;

&lt;p&gt;When you stop, a small Node pipeline turns that into a brief. The interesting step is alignment: each word you said is matched to what you were pointing at when you said it, so "this price" and "that button" resolve to the right crop. Secrets are redacted before anything is written, and if a credential is visible in a screenshot, that screenshot is withheld and the brief says so.&lt;/p&gt;

&lt;p&gt;A coin appears. Drop it on your agent's window and the brief is pasted and sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remembering, without a vector database in the cloud
&lt;/h2&gt;

&lt;p&gt;Every brief gets filed into the piece of work it continues, or starts a new one. Filing runs in two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On your Mac:&lt;/strong&gt; a keyword score (BM25) over each task's text, blended with a meaning score from EmbeddingGemma running on-device through ONNX. That gives a shortlist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small judge:&lt;/strong&gt; the shortlist, with each task's title and recent summaries, goes to Jev, a small model that answers whether the brief is the same work, points back to earlier work ("remember the pricing fix? in that task…"), or is new.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Clear matches join, close calls ask "Same work?" on the card, and nothing you placed by hand is ever moved. The next brief on that task carries a note of where it stands and what was decided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents that report back
&lt;/h2&gt;

&lt;p&gt;Deiko ships a local MCP server with five tools: &lt;code&gt;search_briefs&lt;/code&gt;, &lt;code&gt;list_tasks&lt;/code&gt;, &lt;code&gt;get_task&lt;/code&gt;, &lt;code&gt;get_brief&lt;/code&gt; and &lt;code&gt;save_outcome&lt;/code&gt;. Agents can look up earlier work when they need it, and when they finish a brief they save what they did, decided and left open, which feeds the task's note.&lt;/p&gt;

&lt;p&gt;Agents don't always remember to save, so for Claude Code there's a Stop hook: if the agent is about to finish the turn a brief arrived in without saving, it's asked once to do so. It never asks twice in a row, and it stays quiet in chats that aren't a Deiko brief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy you can check
&lt;/h2&gt;

&lt;p&gt;Screenshots, OCR and accessibility text never leave the Mac. Your voice goes to Whisper (with your own Groq key, through Deiko's relay, or not at all with Apple's on-device recogniser), and filing sends titles and summaries to Jev through the relay, which keeps nothing. Until today you had to take my word for that. Now the relay's code is in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the repo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apps/macos&lt;/code&gt;: the Swift menu-bar app&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packages/core&lt;/code&gt;: the Node pipeline and the MCP server&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packages/alignment&lt;/code&gt;: word-to-pointer alignment (TypeScript)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;services/relay&lt;/code&gt;: the relay (AWS Lambda)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evals&lt;/code&gt;: the filing, search and memory evals I use to check changes&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;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deiko.app/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS 14+ on Apple silicon. Free, with two hours of hosted transcription a month; your own Groq key or on-device transcription works with no limit.&lt;/p&gt;

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

&lt;p&gt;There are a few good first issues open for Hacktoberfest (Zed support, README translations, a Homebrew tap). Issues and feedback are very welcome, especially about where the gesture gets in your way.&lt;/p&gt;

&lt;p&gt;Security note: it asks for Accessibility, Screen Recording and Microphone access and captures only while a session runs (a red bar shows). Screenshots never leave your Mac. It isn't notarized by Apple yet, so read the install script first: &lt;a href="https://github.com/maddy30445r/deiko/blob/main/scripts/install.sh" rel="noopener noreferrer"&gt;https://github.com/maddy30445r/deiko/blob/main/scripts/install.sh&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>software</category>
    </item>
    <item>
      <title>I spent a week teaching my Mac app to remember what I meant last Tuesday</title>
      <dc:creator>Madhur Mittal</dc:creator>
      <pubDate>Fri, 25 Sep 2026 22:48:31 +0000</pubDate>
      <link>https://dev.to/maddy30445r/i-spent-a-week-teaching-my-mac-app-to-remember-what-i-meant-last-tuesday-369c</link>
      <guid>https://dev.to/maddy30445r/i-spent-a-week-teaching-my-mac-app-to-remember-what-i-meant-last-tuesday-369c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7pfbnsce7fdw7d6yhk3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7pfbnsce7fdw7d6yhk3g.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;This happens to me about four times a day.&lt;/p&gt;

&lt;p&gt;I find a bug. I explain it to my coding agent. We fix half of it. Three days later the other half turns up, and I'm explaining the whole thing again from scratch to an agent with the memory of a goldfish.&lt;/p&gt;

&lt;p&gt;"No, not &lt;em&gt;that&lt;/em&gt; price. The one on the pricing page. Remember? We tried the cache thing? It didn't work?"&lt;/p&gt;

&lt;p&gt;It doesn't remember correctly.&lt;/p&gt;

&lt;p&gt;So last week I gave my app a memory. This post covers that week: how I stumbled onto Jev, the homework I did on how other apps remember things, the moment my own board embarrassed me, and the small filing algorithm I ended up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait, what's Deiko?
&lt;/h2&gt;

&lt;p&gt;Deiko is a Mac app I'm building. You double-tap a key, point at things on your screen and talk. Deiko turns that into a brief: your words, screenshots of what you pointed at, and the exact text that was on screen. Then you drag it onto Claude Code, Cursor or whatever agent you use, and it gets pasted in.&lt;/p&gt;

&lt;p&gt;Every brief also lands on a board. That board is where the problem was hiding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with a pile
&lt;/h2&gt;

&lt;p&gt;After a few weeks of use, my board was a pile. Eleven briefs from one design demo sat there as eleven loose cards. Three briefs about the same $99 price bug had, as far as Deiko knew, nothing to do with each other.&lt;/p&gt;

&lt;p&gt;Each brief knew everything about its own moment and nothing about the one before it. What I wanted was easy to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Talk about the same bug on Tuesday and on Friday. Deiko knows it's the same bug, and Friday's agent knows what Tuesday's agent already tried.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Easy to say, anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Jev
&lt;/h2&gt;

&lt;p&gt;Jev kept turning up on my X feed. It's a model from TypeSafe AI that does one narrow thing oddly well: you give it some facts and a typed question like "is this X?", and it gives you back a probability. You don't get an essay or a "Great question!", just a number.&lt;/p&gt;

&lt;p&gt;That's exactly the shape of my problem. "Does this new brief belong to that old task?" is a yes/no question with a confidence attached. It's also cheap: TypeSafe's docs list $0.042 per million input tokens, and output is free. Sorting one brief costs me about a tenth of a cent.&lt;/p&gt;

&lt;p&gt;So I went to sign up, full of momentum, and got this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Whoops, we're full - check &lt;a href="https://x.com/typesafeai" rel="noopener noreferrer"&gt;https://x.com/typesafeai&lt;/a&gt; for more information!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They had paused signups two days after opening them. Cool. Great. Love that for me.&lt;/p&gt;

&lt;p&gt;Luckily the same model is resold at the same price through a few gateways, so I wired in Vercel's AI Gateway, OpenRouter and Cloudflare, and the week carried on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Homework: how does everyone else do this?
&lt;/h2&gt;

&lt;p&gt;Before writing any code I did the thing I usually skip: I read how other tools remember things. I skipped the landing pages and read the mechanisms: the docs, the source where I could, and the GitHub issues where people complain.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where I looked&lt;/th&gt;
&lt;th&gt;What I took&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code's memory, Cursor's memories&lt;/td&gt;
&lt;td&gt;Keep one running note per piece of work, with "where it stands now" at the top and a hard size cap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mem0, Letta, Zep&lt;/td&gt;
&lt;td&gt;Tools that classify every memory from scratch get the most "why did it merge these?!" complaints. Use cheap, exact clues first, and ask a model only when it's actually ambiguous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Obsidian, Notion, Granola&lt;/td&gt;
&lt;td&gt;When a person files something by hand, that choice is final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gmail threading&lt;/td&gt;
&lt;td&gt;Exact clues beat fuzzy guesses. Email has a reply header; Deiko has the page or file you were looking at&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry's issue grouping&lt;/td&gt;
&lt;td&gt;Clean up an error before you match it, so "line 42" and "line 43" count as the same crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search engines&lt;/td&gt;
&lt;td&gt;Blend keyword search with meaning search, because each one catches what the other misses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One bug report stayed with me: a memory tool had saved the same memory 808 times, because it kept re-learning whatever it had just put into the prompt. Lesson noted: never learn back what you injected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three versions in one week
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;v1: chains.&lt;/strong&gt; Each brief pointed at the one it continued. The result looked like beads on a string, and it barely helped. The eleven demo briefs were still eleven cards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v2: tasks.&lt;/strong&gt; Now a brief belongs to a task, and a task belongs to a project. Deiko keeps a note for each task: where it stands, what was decided, what was tried. When the agent finishes, it writes its outcome back into that note. To pick the task, I showed Jev a shortlist of 8 and asked "which one is this most like?"&lt;/p&gt;

&lt;p&gt;I shipped it, re-sorted my real board and opened it up feeling rather proud.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where my own board embarrassed me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A question about a signup chart ("so here in week 32 chart...") was filed under &lt;strong&gt;Pricing&lt;/strong&gt;, at 86% confidence.&lt;/li&gt;
&lt;li&gt;A mic check, which was literally me saying "testing, testing, can you hear me", was filed as real work. Under Pricing too, naturally.&lt;/li&gt;
&lt;li&gt;Every brief lived in a project called "Deiko", including the ones that had nothing to do with Deiko.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I dug in, every mistake was mine, not Jev's.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I asked a question that always has a winner.&lt;/strong&gt; "Which of these 8 tasks is it most like?" is like asking "which of these 8 people is your cousin?" when none of them is. Someone gets picked anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing asked "is this even a request?"&lt;/strong&gt; So "hello hello" got filed like a bug report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My shortlist favoured recent work over related work.&lt;/strong&gt; Pricing was always in the running because I'd touched it recently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One wrong join made the next one worse.&lt;/strong&gt; I described each task by its newest brief. Once the mic test joined Pricing, Jev saw Pricing described as "they are testing the microphone". It snowballed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I was ignoring my best clue.&lt;/strong&gt; Chrome tells Deiko the page name ("Signups — build", "Pricing — build"). I saved it and never used it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I was giving Jev a multiple-choice quiz when it's at its best with yes or no.&lt;/p&gt;

&lt;h2&gt;
  
  
  v3: a very careful librarian
&lt;/h2&gt;

&lt;p&gt;v3 files a brief the way a careful librarian files a note.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Is it a note at all?&lt;/strong&gt; Jev answers "is this a real request?" Mic checks, greetings and thank-yous go to a pile called &lt;em&gt;mic checks &amp;amp; scraps&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Read the labels.&lt;/strong&gt; Before any AI gets involved, Deiko reads exact labels off what it already recorded: the page, the file, the project, the error, the ticket number. Each one is tidied so two visits match. "(3) Signups — build" becomes "Signups". "/users/8812" becomes "/users/*". An error loses its line number (thanks, Sentry). Titles that don't name a real page, like "New Tab" or "Dashboard", don't count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cast a wide net.&lt;/strong&gt; Up to 20 tasks get a seat at the table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An exact label match gets a seat outright, unless that label is on more than 5 tasks. Looking at you, &lt;code&gt;index.tsx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Time words like "yesterday" or "last week" get a few seats. So do "kal" and "pichle hafte", because I narrate half my briefs in Hinglish.&lt;/li&gt;
&lt;li&gt;The remaining seats go to a blend of keyword search (BM25) and meaning search. The meaning part is a small model that runs on your Mac (Microsoft's harrier-oss-v1-270m, about 200 MB, around 70 ms a brief), so "the graph thing" still finds your chart work even when no word matches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recency gets no seats any more. It only breaks ties.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Ask yes or no, once per task.&lt;/strong&gt; In one request, Jev answers "is this the same piece of work?" for each of the 20 tasks. Every task gets its own probability, and they don't have to add up to 100%. The question says plainly that the same app or the same topic isn't enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Look twice at the finalists.&lt;/strong&gt; The one or two tasks that scored 35% or more each get a private second request containing just the new brief and that one task, so nothing else can sway the answer. It asks the same question again, plus one more: are these different, related but separate, or the same work?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Decide in boring code.&lt;/strong&gt; Jev only gives numbers. The rules that turn numbers into decisions live in my code, in one place, with tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// below this, it's a mic check&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;JOIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;recentMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;e3&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ASK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// between this and a join: ask the human&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;A brief &lt;strong&gt;joins&lt;/strong&gt; a task only when the second look says "same", round one is at 60% or more, and that task is 20 points ahead of the runner-up.&lt;/li&gt;
&lt;li&gt;Work from the &lt;strong&gt;last 30 minutes&lt;/strong&gt; on the same page or file joins a little more easily. Old tasks never get that break, so they can't quietly swallow new work.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;different ticket number&lt;/strong&gt; (ENG-142 against ENG-150) never joins.&lt;/li&gt;
&lt;li&gt;For a &lt;strong&gt;maybe&lt;/strong&gt;, the brief starts on its own and its card asks &lt;strong&gt;"Which one?"&lt;/strong&gt; with the likely tasks as buttons. One click fixes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Related but separate&lt;/strong&gt; work gets a "Related to Pricing" link instead of a merge.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Plot twist: Jev is shy
&lt;/h2&gt;

&lt;p&gt;My design doc said "join when the second look is 90% sure". Sensible and principled.&lt;/p&gt;

&lt;p&gt;It joined nothing. Zero briefs.&lt;/p&gt;

&lt;p&gt;Before building v3 I had made a test set from my own board: real briefs, each labelled by hand with where it belongs. Every change had to beat the previous run. On the ten briefs that truly belonged together, Jev's one-on-one "yes" sat between 0.43 and 0.88. It said "same" every single time. It just wouldn't say it loudly. Meanwhile, every brief that really was new had its best match at 0.23 or lower.&lt;/p&gt;

&lt;p&gt;So the numbers in that snippet came from my data, not from a hunch. If you take one thing from this post, make it this: &lt;strong&gt;build the test set first.&lt;/strong&gt; I was confidently and completely wrong about the thresholds.&lt;/p&gt;

&lt;p&gt;Where it stands now: 28 of the 33 labelled briefs land where I'd put them by hand, and about one real brief in nine gets a "Which one?" card. I watch that second number closely. If it creeps past one in four, people start clicking anything to make the card go away, and then memory is worse than no memory.&lt;/p&gt;
&lt;h2&gt;
  
  
  Letting the agent dig for itself
&lt;/h2&gt;

&lt;p&gt;Filing will sometimes be unsure, so the agent shouldn't have to depend on a single guess.&lt;/p&gt;

&lt;p&gt;When a brief joins a task, the agent is told: &lt;em&gt;this carries on from "Pricing display bug"&lt;/em&gt;, where it stands, and the last few briefs, one line each. When it's only a hunch, the prompt says so: &lt;em&gt;possibly related, not confirmed&lt;/em&gt;. The whole memory section stays under 20 lines.&lt;/p&gt;

&lt;p&gt;There's also a small memory helper, an MCP server that runs only on your Mac, with three tools: &lt;code&gt;search_briefs&lt;/code&gt;, &lt;code&gt;get_task&lt;/code&gt; and &lt;code&gt;get_brief&lt;/code&gt;. Say "like the chart thing from last week" and your agent can go and find it itself. One button in Settings sets it up for every agent Deiko finds on your Mac.&lt;/p&gt;

&lt;p&gt;The search covers everything on your Mac, but it hands back much less, because the answers go to your agent's cloud model. You get your briefs, the agents' notes and the screenshots you kept. Screenshots you removed stay removed, audio never leaves, and anything that looks like a secret is scrubbed first. That last rule isn't hypothetical: our security audit once found a live API key in text Deiko had captured from the screen.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it feels like now
&lt;/h2&gt;

&lt;p&gt;On Thursday you point at the pricing page and say "the price still shows $99 after I edit it". Your agent tries a cache fix. On Monday you point at the same page: "still broken."&lt;/p&gt;

&lt;p&gt;Deiko files Monday's brief under &lt;em&gt;Pricing display bug&lt;/em&gt;. Monday's agent gets the note saying the cache fix was tried and didn't work, so it starts from there instead of from zero. You didn't have to explain anything twice.&lt;/p&gt;
&lt;h2&gt;
  
  
  Things I'd tell myself a week ago
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ask yes or no, never "pick one". A pick-one question always has a winner.&lt;/li&gt;
&lt;li&gt;Use exact clues before fuzzy ones: page names, file names, ticket numbers.&lt;/li&gt;
&lt;li&gt;Describe things by what you're sure of, never by whatever came in last.&lt;/li&gt;
&lt;li&gt;Keep the decision in your own code, with the numbers in one place.&lt;/li&gt;
&lt;li&gt;Build the test set before the feature. Your thresholds are wrong.&lt;/li&gt;
&lt;li&gt;When you're unsure, ask the human once, with buttons.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Deiko runs on macOS 14 or later, and Pro is free until 24 October 2026.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deiko.app/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The site if you want more info - &lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://deiko.app/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeiko.app%2Fassets%2Fog.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://deiko.app/" rel="noopener noreferrer" class="c-link"&gt;
            Deiko: show, don’t type
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Point at anything on your screen and say what should change. Deiko throws your coding agent a brief with the screenshot attached.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520xmlns%3D%2527http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%2527%2520viewBox%3D%25270%25200%252032%252032%2527%253E%253Cdefs%253E%253ClinearGradient%2520id%3D%2527g%2527%2520x2%3D%25270%2527%2520y2%3D%25271%2527%253E%253Cstop%2520stop-color%3D%2527%25235a6bc4%2527%2F%253E%253Cstop%2520offset%3D%25271%2527%2520stop-color%3D%2527%252337448c%2527%2F%253E%253C%2FlinearGradient%253E%253C%2Fdefs%253E%253Crect%2520width%3D%252732%2527%2520height%3D%252732%2527%2520rx%3D%25277.2%2527%2520fill%3D%2527url%28%2523g%29%2527%2F%253E%253Ccircle%2520cx%3D%252716%2527%2520cy%3D%252716%2527%2520r%3D%25277.1%2527%2520fill%3D%2527none%2527%2520stroke%3D%2527%2523eef1ff%2527%2520stroke-width%3D%25271.9%2527%2F%253E%253Ccircle%2520cx%3D%252716%2527%2520cy%3D%252716%2527%2520r%3D%25272%2527%2520fill%3D%2527%2523eef1ff%2527%2F%253E%253C%2Fsvg%253E" width="" height=""&gt;
          deiko.app
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;If you've built memory for anything, I'd love to hear what broke for you, and which of my thresholds you think will age worst. Leave a comment below.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>buildinpublic</category>
      <category>productivity</category>
      <category>jev</category>
    </item>
    <item>
      <title>I am tired of taking screenshots and explaining them to Claude code while working.</title>
      <dc:creator>Madhur Mittal</dc:creator>
      <pubDate>Thu, 17 Sep 2026 18:21:24 +0000</pubDate>
      <link>https://dev.to/maddy30445r/i-am-tired-of-taking-screenshots-and-explaining-them-to-claude-code-while-working-4bio</link>
      <guid>https://dev.to/maddy30445r/i-am-tired-of-taking-screenshots-and-explaining-them-to-claude-code-while-working-4bio</guid>
      <description>&lt;p&gt;Okay so a little context first,&lt;br&gt;
I work in an &lt;strong&gt;SDE role&lt;/strong&gt; and work in multiple windows at once like i have many apps open in my mac.&lt;br&gt;
I faced a problem of &lt;strong&gt;screenshotting&lt;/strong&gt; everything and explaining the context and their relations to &lt;em&gt;claude code&lt;/em&gt; again and again like for little UI changes and such or any bug.&lt;br&gt;
I got this idea and started working on it , made a swift app which will track cursor and when your press a hotkey it starts only then and when you hold a hotkey it will draw over the areas your point with your cursor and you can keep talking while explaining what you need like you would to a real colleague. The voice timings come from whisper and they get attached with the cursor positions and screenshots and form a full event files.&lt;br&gt;
those get stored as context everytime you do some actions like draw a circle , point to anything or something like that.&lt;br&gt;
and at the end we generate a context using those events that you can just drop to any AI agent of your choice like claude,cursor,codex etc. and it just starts working on it right off the bat.&lt;/p&gt;

&lt;p&gt;A bit on how it works&lt;br&gt;
The hard part was never the screenshot. It's the word "this".&lt;/p&gt;

&lt;p&gt;"Add a status key to this response" means nothing on its own. It only works if you know what "this" was aimed at. So while a session runs I keep two streams: where the cursor settled, and what you were saying at the time. Then I have to figure out which settles were actually pointing at something and which were just my hand sitting on the trackpad. Cursor position alone can't tell you that.&lt;/p&gt;

&lt;p&gt;What worked was watching for the pointing words. If "this" or "here" lands on a settle, that settle was a pointing act. If a settle sits in the middle of silence, it was nothing. That binding is most of the app.&lt;/p&gt;

&lt;p&gt;For the screenshot side I use AXUIElementCopyElementAtPosition against a system-wide element, which gives me whatever is under the cursor plus its accessibility label and window title. SCScreenshotManager crops the pixels and Vision runs OCR on the crop. So the brief ends up carrying the name of the thing you pointed at and not just a picture of it, which is usually what the agent was guessing at anyway.&lt;/p&gt;

&lt;p&gt;There are no accounts. The trial is keyed to a hash of the machine's platform UUID rather than anything in preferences, so deleting a plist doesn't get you a fresh one. Screenshots, OCR text and the brief never leave the Mac. The audio is the only thing that goes anywhere and it's gone as soon as the text exists.&lt;/p&gt;

&lt;p&gt;Here is the producthunt page for more context on it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://www.producthunt.com/products/deiko?launch=deiko" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;producthunt.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;And below is the site to get it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://deiko.app/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeiko.app%2Fassets%2Fog.png" height="420" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://deiko.app/" rel="noopener noreferrer" class="c-link"&gt;
            Deiko: show, don’t type
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Point at anything on your screen and say what should change. Deiko throws your coding agent a brief with the screenshot attached.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520xmlns%3D%2527http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%2527%2520viewBox%3D%25270%25200%252032%252032%2527%253E%253Cdefs%253E%253ClinearGradient%2520id%3D%2527g%2527%2520x2%3D%25270%2527%2520y2%3D%25271%2527%253E%253Cstop%2520stop-color%3D%2527%25235a6bc4%2527%2F%253E%253Cstop%2520offset%3D%25271%2527%2520stop-color%3D%2527%252337448c%2527%2F%253E%253C%2FlinearGradient%253E%253C%2Fdefs%253E%253Crect%2520width%3D%252732%2527%2520height%3D%252732%2527%2520rx%3D%25277.2%2527%2520fill%3D%2527url%28%2523g%29%2527%2F%253E%253Ccircle%2520cx%3D%252716%2527%2520cy%3D%252716%2527%2520r%3D%25277.1%2527%2520fill%3D%2527none%2527%2520stroke%3D%2527%2523eef1ff%2527%2520stroke-width%3D%25271.9%2527%2F%253E%253Ccircle%2520cx%3D%252716%2527%2520cy%3D%252716%2527%2520r%3D%25272%2527%2520fill%3D%2527%2523eef1ff%2527%2F%253E%253C%2Fsvg%253E" width="" height=""&gt;
          deiko.app
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I am thinking of providing a free version with BYO option and a lifetime subscription for around 40ish dollars. &lt;br&gt;
Need some advice and people who face this problem and what do they generally use to solve it?&lt;/p&gt;

</description>
      <category>claude</category>
      <category>vibecoding</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
