<?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: E Daugherty LLC</title>
    <description>The latest articles on DEV Community by E Daugherty LLC (@jeff_feldman_7ab3898).</description>
    <link>https://dev.to/jeff_feldman_7ab3898</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%2F4241938%2F43028f49-4a7e-44e9-80d2-6485b6c2d69e.png</url>
      <title>DEV Community: E Daugherty LLC</title>
      <link>https://dev.to/jeff_feldman_7ab3898</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jeff_feldman_7ab3898"/>
    <language>en</language>
    <item>
      <title>Which Coding Agent Is Waiting on You? Wiring Claude Code and Codex Into One Status Board on a Mac</title>
      <dc:creator>E Daugherty LLC</dc:creator>
      <pubDate>Fri, 13 Nov 2026 19:23:22 +0000</pubDate>
      <link>https://dev.to/jeff_feldman_7ab3898/which-coding-agent-is-waiting-on-you-wiring-claude-code-and-codex-into-one-status-board-on-a-mac-12ip</link>
      <guid>https://dev.to/jeff_feldman_7ab3898/which-coding-agent-is-waiting-on-you-wiring-claude-code-and-codex-into-one-status-board-on-a-mac-12ip</guid>
      <description>&lt;p&gt;Running two or three coding agents at once sounds efficient until you count the dead time. One terminal finished four minutes ago and is sitting at a prompt. Another stopped to ask whether it may run &lt;code&gt;npm install&lt;/code&gt;, and you never saw the question because that tab was behind your editor. The third is still working, so you keep checking it, which is the only one that did not need you.&lt;/p&gt;

&lt;p&gt;The expensive part is not the model. It is the gap between "the agent needs a human" and "the human notices." This post shows how to close that gap with the hooks both tools already ship, using nothing but a shell script, &lt;code&gt;jq&lt;/code&gt;, and a folder of small JSON files. You can copy it as is. At the end I show the view I use for the same job, but nothing here depends on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three states that matter
&lt;/h2&gt;

&lt;p&gt;Every session you care about is in one of three states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Working.&lt;/strong&gt; Leave it alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Needs you.&lt;/strong&gt; It is blocked on a permission prompt or a question. Every minute here is wasted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Done, your turn.&lt;/strong&gt; It finished the turn and wants review or the next instruction.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A useful board only has to answer one question: which sessions are in state 2 or 3, and in which folder?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude Code gives you
&lt;/h2&gt;

&lt;p&gt;Claude Code runs &lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks" rel="noopener noreferrer"&gt;hooks&lt;/a&gt; at fixed points in a session. A hook is a command that receives JSON on stdin. Every event includes &lt;code&gt;session_id&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt; and &lt;code&gt;hook_event_name&lt;/code&gt;, which is exactly what a board needs to tell sessions apart.&lt;/p&gt;

&lt;p&gt;Three events map onto the three states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UserPromptSubmit&lt;/code&gt; fires when a prompt is submitted. That means &lt;strong&gt;working&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Notification&lt;/code&gt; fires when Claude Code sends a notification. Its matcher is the notification type, and the documented types include &lt;code&gt;permission_prompt&lt;/code&gt; and &lt;code&gt;idle_prompt&lt;/code&gt;. That means &lt;strong&gt;needs you&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Stop&lt;/code&gt; fires when Claude finishes responding. That means &lt;strong&gt;done, your turn&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put this in &lt;code&gt;~/.claude/settings.json&lt;/code&gt; so it applies to every project on the machine (a project's &lt;code&gt;.claude/settings.json&lt;/code&gt; works too, but then it only covers that repo):&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="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"UserPromptSubmit"&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="nl"&gt;"hooks"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"~/.agent-status/report.sh working"&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="nl"&gt;"Notification"&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="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"permission_prompt|idle_prompt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;"hooks"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"~/.agent-status/report.sh needs_you"&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="nl"&gt;"Stop"&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="nl"&gt;"hooks"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"~/.agent-status/report.sh done"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The reporter script
&lt;/h2&gt;

&lt;p&gt;One file per session, overwritten on every event. No database, no daemon.&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="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# ~/.agent-status/report.sh &amp;lt;state&amp;gt;&lt;/span&gt;
&lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$1"&lt;/span&gt;
&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;cat&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$HOME/.agent-status/sessions"&lt;/span&gt;
mkdir -p &lt;span class="s2"&gt;"$dir"&lt;/span&gt;

&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;jq -r &lt;span class="s1"&gt;'.session_id // empty'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"$input"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; -z &lt;span class="s2"&gt;"$id"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 0

jq -nc \
  --arg state &lt;span class="s2"&gt;"$state"&lt;/span&gt; \
  --arg agent &lt;span class="s2"&gt;"claude"&lt;/span&gt; \
  --arg cwd &lt;span class="s2"&gt;"$(jq -r '.cwd // ""' &amp;lt;&amp;lt;&amp;lt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;")"&lt;/span&gt; \
  --arg msg &lt;span class="s2"&gt;"$(jq -r '.message // ""' &amp;lt;&amp;lt;&amp;lt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;")"&lt;/span&gt; \
  --arg at &lt;span class="s2"&gt;"$(date +%s)"&lt;/span&gt; \
  &lt;span class="s1"&gt;'{state:$state, agent:$agent, cwd:$cwd, msg:$msg, at:($at|tonumber)}'&lt;/span&gt; \
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"$dir/$id.json.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mv &lt;span class="s2"&gt;"$dir/$id.json.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"$dir/$id.json"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$state"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"needs_you"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  osascript -e &lt;span class="s2"&gt;"display notification \"$(basename "&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;jq -r &lt;span class="s1"&gt;'.cwd // ""'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"$input"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;") needs you\" with title \"Claude Code\""&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="nb"&gt;exit&lt;/span&gt; 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details that save debugging time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write to a temp file, then &lt;code&gt;mv&lt;/code&gt;.&lt;/strong&gt; A reader that lists the folder mid write will otherwise hit half a JSON object. &lt;code&gt;mv&lt;/code&gt; within one volume is atomic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit 0 and stay fast.&lt;/strong&gt; Hooks run inline with the session. A slow hook is a slow agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't write to &lt;code&gt;/dev/tty&lt;/code&gt;.&lt;/strong&gt; The docs are explicit that hooks run without a controlling terminal. If you want a terminal-native alert instead of &lt;code&gt;osascript&lt;/code&gt;, return a &lt;code&gt;terminalSequence&lt;/code&gt; field from the hook and Claude Code writes the escape sequence for you, in an interactive session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;chmod +x ~/.agent-status/report.sh&lt;/code&gt; and start a session. You should see a file appear in &lt;code&gt;~/.agent-status/sessions/&lt;/code&gt; on your first prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Codex gives you, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;Codex has a &lt;a href="https://developers.openai.com/codex/config-advanced" rel="noopener noreferrer"&gt;&lt;code&gt;notify&lt;/code&gt; setting&lt;/a&gt; that runs an external program with a JSON argument. The documented catch: it currently fires for one event, &lt;code&gt;agent-turn-complete&lt;/code&gt;. The payload includes &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;thread-id&lt;/code&gt;, &lt;code&gt;turn-id&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;input-messages&lt;/code&gt; and &lt;code&gt;last-assistant-message&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So Codex can tell your board &lt;strong&gt;done&lt;/strong&gt;, keyed by &lt;code&gt;thread-id&lt;/code&gt;, but &lt;code&gt;notify&lt;/code&gt; cannot tell it &lt;strong&gt;needs you&lt;/strong&gt;. Add it to your user-level &lt;code&gt;~/.codex/config.toml&lt;/code&gt; (Codex ignores &lt;code&gt;notify&lt;/code&gt; in a project-local config and warns about it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="na"&gt;notify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/bin/bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"~/.agent-status/codex.sh \"$1\""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"codex-notify"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# ~/.agent-status/codex.sh '&amp;lt;json&amp;gt;'&lt;/span&gt;
&lt;span class="nv"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$1"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$(jq -r '.type' &amp;lt;&amp;lt;&amp;lt;"&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="s2"&gt;")"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"agent-turn-complete"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 0
&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;jq -r &lt;span class="s1"&gt;'."thread-id"'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"$payload"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$HOME/.agent-status/sessions"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; mkdir -p &lt;span class="s2"&gt;"$dir"&lt;/span&gt;
jq -nc --arg cwd &lt;span class="s2"&gt;"$(jq -r '.cwd // ""' &amp;lt;&amp;lt;&amp;lt;"&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="s2"&gt;")"&lt;/span&gt; --arg at &lt;span class="s2"&gt;"$(date +%s)"&lt;/span&gt; \
  &lt;span class="s1"&gt;'{state:"done", agent:"codex", cwd:$cwd, msg:"", at:($at|tonumber)}'&lt;/span&gt; \
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"$dir/codex-$id.json.tmp"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mv &lt;span class="s2"&gt;"$dir/codex-$id.json.tmp"&lt;/span&gt; &lt;span class="s2"&gt;"$dir/codex-$id.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For approvals, lean on the TUI instead. Codex's built-in &lt;code&gt;tui.notifications&lt;/code&gt; can filter by event type, including &lt;code&gt;approval-requested&lt;/code&gt;, and &lt;code&gt;tui.notification_condition&lt;/code&gt; controls whether it fires only when the terminal is unfocused. That gives you an alert, not a board entry, which is an honest limit worth knowing before you build around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the board
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# ~/.agent-status/board&lt;/span&gt;
&lt;span class="nv"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;date +%s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; f &lt;span class="k"&gt;in&lt;/span&gt; ~/.agent-status/sessions/*.json&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; -e &lt;span class="s2"&gt;"$f"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;continue&lt;/span&gt;
  jq -r --argjson now &lt;span class="s2"&gt;"$now"&lt;/span&gt; \
    &lt;span class="s1"&gt;'[.state, .agent, (.cwd|split("/")|last), (($now-.at)/60|floor|tostring)+"m ago"] | @tsv'&lt;/span&gt; &lt;span class="s2"&gt;"$f"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; sort &lt;span class="o"&gt;|&lt;/span&gt; column -t
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sorting puts &lt;code&gt;done&lt;/code&gt; and &lt;code&gt;needs_you&lt;/code&gt; above &lt;code&gt;working&lt;/code&gt; alphabetically, which happens to be the order you want. Run it with &lt;code&gt;watch -n 5 ~/.agent-status/board&lt;/code&gt; in a spare pane, or bind it to a key.&lt;/p&gt;

&lt;p&gt;Two cleanup rules keep it honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expire stale files.&lt;/strong&gt; A session you closed with Ctrl+C never fires &lt;code&gt;Stop&lt;/code&gt;. Drop anything older than a few hours: &lt;code&gt;find ~/.agent-status/sessions -mmin +240 -delete&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat "working" as a claim, not a fact.&lt;/strong&gt; It only means the last event was a prompt. If a process crashed, the file still says working. Age is your sanity check.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this breaks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud sessions.&lt;/strong&gt; Claude Code's docs note that cloud sessions don't read your local &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, so those sessions never reach your board.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subagents.&lt;/strong&gt; Tool hooks also fire inside subagents, with &lt;code&gt;agent_id&lt;/code&gt; set. If you later add &lt;code&gt;PreToolUse&lt;/code&gt; reporting, key on it or one subagent will overwrite the parent's state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex approvals.&lt;/strong&gt; Covered above: &lt;code&gt;notify&lt;/code&gt; will not see them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The version I look at
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://tokenbar.site/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=agent-waiting-board" rel="noopener noreferrer"&gt;TokenBar&lt;/a&gt;, a macOS 14+ menu bar app that shows usage and reset times for Claude, Codex, Cursor, Gemini and Copilot. It is built on Peter Steinberger's MIT-licensed &lt;a href="https://github.com/steipete/CodexBar" rel="noopener noreferrer"&gt;CodexBar&lt;/a&gt;. It also has a panel for &lt;a href="https://github.com/pixel-agents-hq/pixel-agents" rel="noopener noreferrer"&gt;Pixel Agents&lt;/a&gt;, a separate MIT open-source project that draws coding sessions as people at desks. The panel connects to your own local Pixel Agents build (Node.js 20 or newer) rather than bundling it.&lt;/p&gt;

&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%2F0dw09dj2qsjdxesyutdu.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%2F0dw09dj2qsjdxesyutdu.png" alt="TokenBar Pixel Agents panel showing desks labeled working, done and needs your OK, with a Needs You list on the right. Demo mode with fictional projects." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshot is demo mode with fictional projects. It sorts the same three states this post builds by hand: who is working, who is done and waiting for your turn, and who needs your OK. If the script above is enough for you, keep the script. TokenBar is $5/month Basic or $15 lifetime Pro and does not add any quota to your plans.&lt;/p&gt;

&lt;p&gt;Disclosure: I build TokenBar.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>ai</category>
      <category>bash</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Metering a Free Tier by Words Instead of Days in a Mac App</title>
      <dc:creator>E Daugherty LLC</dc:creator>
      <pubDate>Fri, 13 Nov 2026 17:21:13 +0000</pubDate>
      <link>https://dev.to/jeff_feldman_7ab3898/metering-a-free-tier-by-words-instead-of-days-in-a-mac-app-1997</link>
      <guid>https://dev.to/jeff_feldman_7ab3898/metering-a-free-tier-by-words-instead-of-days-in-a-mac-app-1997</guid>
      <description>&lt;p&gt;On November 11, DraftKey shipped two releases about six hours apart. Version 0.1.61 made the app free to try with 2,000 words of writing help per Mac. Version 0.1.62 cut that to 500 words, added a warning when about 100 words are left, and changed what the app looks like once the allowance runs out. Both release notes are public, and they are the source for every DraftKey detail in this post.&lt;/p&gt;

&lt;p&gt;We build DraftKey, a Mac app for Tab autocomplete and rewrites you review before they are inserted. This is not a pricing post. It is about the engineering decisions hiding behind a sentence like "every Mac gets 500 free words," because almost none of them are obvious until you have to ship one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why words, not days
&lt;/h2&gt;

&lt;p&gt;The default trial for desktop software is a clock: 7 or 14 days, then a paywall. A clock is easy to build and easy to explain. It is also a poor match for a tool you use in short bursts. Someone installs a writing helper on a Monday, uses it twice, gets busy, and comes back on day 15 to a locked app they never really tried.&lt;/p&gt;

&lt;p&gt;A usage allowance is consumed only when the feature does work. Nobody loses their trial to a vacation. The tradeoff is that you now have to define the unit precisely, count it reliably, and show the count somewhere people will see it. A day counts itself. A word does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what is metered, and fence off what is not
&lt;/h2&gt;

&lt;p&gt;The first design question is scope. DraftKey's notes list what the allowance covers: suggestions, Writing tools, and slash commands. They are just as explicit about what it does not cover: keyboard sounds and clipboard history "stay free and are never counted."&lt;/p&gt;

&lt;p&gt;That second list matters more than it looks. If a feature is promised as free, it should be impossible for it to touch the meter, not merely unlikely. In practice that means the counting call lives in one place, on the path where model output is actually delivered, rather than being sprinkled through the app. Features that never reach that path cannot decrement the allowance by accident.&lt;/p&gt;

&lt;p&gt;You also have to pick a counting rule and stick to it. Words typed by the user, words suggested, or words actually accepted are three different numbers. Whatever you pick, make it the one a user would guess if you asked them, because the count is going to be displayed and they will compare it with their own sense of how much they used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Device identity on macOS, without shipping a hardware ID
&lt;/h2&gt;

&lt;p&gt;"The free word count belongs to your Mac" sounds simple. It requires a stable answer to "which Mac is this?" that survives an app reinstall, because a counter that resets when someone drags the app to the Trash and back is not much of a limit.&lt;/p&gt;

&lt;p&gt;macOS gives you a few options, each with costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A random UUID you generate and store.&lt;/strong&gt; UserDefaults is wiped by deleting the app's preferences. The Keychain survives reinstalls but can be removed by the user. Either is fine for convenience and weak as an allowance anchor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hardware-derived identifier.&lt;/strong&gt; IOKit exposes a platform UUID on the &lt;code&gt;IOPlatformExpertDevice&lt;/code&gt; registry entry (the &lt;code&gt;IOPlatformUUID&lt;/code&gt; property), which stays the same across reinstalls of your app. It is also a persistent identifier for that machine, which is exactly why it should never leave the Mac in raw form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An account.&lt;/strong&gt; Requiring sign-in solves identity cleanly and adds friction to the first minute, which is the minute a free tier exists to protect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever source you use, send a derived value instead of the raw one. A keyed hash, such as an HMAC from Apple's &lt;a href="https://developer.apple.com/documentation/cryptokit/hmac" rel="noopener noreferrer"&gt;CryptoKit&lt;/a&gt; with a key specific to your app, gives your server a stable identifier for the Mac without handing it a hardware ID that could be matched against other services. DraftKey's notes describe this part in plain terms: it checks the count online "with a hashed identifier for your Mac and a number of words, never your writing." That is the whole payload a meter like this needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send counts, not content
&lt;/h2&gt;

&lt;p&gt;A writing app has an obvious temptation here: the text is right there, so why not send it and count server-side? Because it turns a billing feature into a data-collection feature. The server only needs two facts to enforce an allowance: who is asking (the hashed device value) and how many units were used. Keep the request that small and the privacy story of the meter is easy to state and easy to verify by watching the traffic.&lt;/p&gt;

&lt;p&gt;Batching helps too. Reporting after every accepted suggestion makes the meter chatty and fragile on bad networks. Reporting accumulated counts periodically, with the server holding the authoritative total, keeps the request rate low. You will still have to decide what happens offline: whether to allow a small local grace amount or pause until the count can be confirmed. Neither choice is wrong, but it should be a decision, not whatever the networking code happens to do on timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warn before zero
&lt;/h2&gt;

&lt;p&gt;For most metered products, running out is an annoyance. For an autocomplete tool it can look like a malfunction. Suggestions simply stop appearing, in the middle of a sentence, and the most reasonable conclusion a user can draw is that the app broke.&lt;/p&gt;

&lt;p&gt;0.1.62 addresses that from two directions. DraftKey gives a heads-up when about 100 free words are left, "so writing help never stops mid-sentence without warning." And once the words are gone, Autocomplete and Writing tools "say they are paused, with a lock, once the free words are gone, instead of looking broken."&lt;/p&gt;

&lt;p&gt;That second change is worth copying even if you never meter anything. Every state a feature can be in should be visibly distinct: on, off by choice, unavailable because of a permission, unavailable because of the plan. If two of those look the same, your support inbox becomes the place where users find out which one they hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the meter where the decision happens
&lt;/h2&gt;

&lt;p&gt;The same release gave the free plan its own card on Home: how many words are left, what stays free forever, what a license adds, and a single button to buy it. License activation moved to that card as well. Home and the menu bar both show the remaining count.&lt;/p&gt;

&lt;p&gt;The engineering point is co-location. The number, the explanation of what it limits, and the way to remove the limit belong in one place. Splitting them across Settings, an About pane, and an email after purchase means users have to reassemble the model in their heads, and many will not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assume the number will change
&lt;/h2&gt;

&lt;p&gt;The allowance went from 2,000 to 500 words between two releases on the same day. Whatever your first number is, plan for it to move. That suggests a few concrete things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the allowance value out of scattered UI strings, so a change does not mean hunting for every place "2,000" was typed.&lt;/li&gt;
&lt;li&gt;Decide up front how a change applies to people who already started: whether the server stores words used or words remaining determines whether a reduction can push someone straight to zero.&lt;/li&gt;
&lt;li&gt;Write the release note in plain numbers. "The free allowance is 500 words of writing help per Mac, down from 2,000" is the kind of sentence that prevents a confused support thread.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I would carry into any usage-metered desktop app
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Meter only the expensive path, and make free features structurally unable to touch the counter.&lt;/li&gt;
&lt;li&gt;Pick a unit users would count the same way you do.&lt;/li&gt;
&lt;li&gt;Anchor the allowance to the device with a derived, keyed value, never a raw hardware identifier.&lt;/li&gt;
&lt;li&gt;Send a count and an identifier, nothing else.&lt;/li&gt;
&lt;li&gt;Warn before the limit, and make "paused by plan" look different from "broken."&lt;/li&gt;
&lt;li&gt;Keep the count, the explanation, and the upgrade path together.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where DraftKey fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: I build DraftKey. It needs an Apple Silicon Mac on macOS 14 or later, and its writing features need at least 8 GB of memory and a separate writing-model download of about 1.12 GB, with at least 2.2 GB of free space. Each Mac gets 500 free words of writing help, and a license is $4.99 once for up to three Macs. It works in compatible text fields, not every app. You can &lt;a href="https://getdraftkey.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=word-metered-free-tier" rel="noopener noreferrer"&gt;see how it works here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have shipped a usage-based trial in a desktop app, what unit did you meter, and how did you handle the moment someone went offline mid-allowance?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DraftKey 0.1.61 and 0.1.62 release notes: &lt;a href="https://github.com/auratechnologiesllc/DraftKey/releases" rel="noopener noreferrer"&gt;https://github.com/auratechnologiesllc/DraftKey/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple, CryptoKit HMAC: &lt;a href="https://developer.apple.com/documentation/cryptokit/hmac" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/cryptokit/hmac&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple, IOKit: &lt;a href="https://developer.apple.com/documentation/iokit" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/iokit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>indiehackers</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Don't Collect Another API Key: Reusing the AI Logins Already on a Mac</title>
      <dc:creator>E Daugherty LLC</dc:creator>
      <pubDate>Fri, 13 Nov 2026 16:10:38 +0000</pubDate>
      <link>https://dev.to/jeff_feldman_7ab3898/dont-collect-another-api-key-reusing-the-ai-logins-already-on-a-mac-3fop</link>
      <guid>https://dev.to/jeff_feldman_7ab3898/dont-collect-another-api-key-reusing-the-ai-logins-already-on-a-mac-3fop</guid>
      <description>&lt;p&gt;The remaining allowance on a coding agent is not a secret. It already lives behind a login you completed in that vendor's own app or CLI. Claude Code has &lt;code&gt;/usage&lt;/code&gt;. Codex has &lt;code&gt;/status&lt;/code&gt;. Cursor, Gemini, and GitHub Copilot each have a usage page of their own. If a menu bar meter asked you to paste a fresh API key for every one of those tools, it would be asking you to open a second billing path and to store a second secret, just to read a number you can already see.&lt;/p&gt;

&lt;p&gt;We build TokenBar, a macOS 14+ menu bar app at Aura Technologies that shows usage and reset times for Claude, Codex, Cursor, Gemini, and Copilot. It is built on &lt;a href="https://github.com/steipete/CodexBar" rel="noopener noreferrer"&gt;CodexBar&lt;/a&gt;, Peter Steinberger's MIT-licensed project, and it can connect to a local install of &lt;a href="https://github.com/pixel-agents-hq/pixel-agents" rel="noopener noreferrer"&gt;Pixel Agents&lt;/a&gt;, a separate MIT-licensed project. TokenBar does not add any quota. This post is about a design choice we kept in front of us while wiring those providers up: reuse the login that is already on the Mac, instead of collecting a new API key as the normal path.&lt;/p&gt;

&lt;h2&gt;
  
  
  An API key is often the wrong kind of credential
&lt;/h2&gt;

&lt;p&gt;For many of these tools, the subscription you already pay for and an API key are not the same thing.&lt;/p&gt;

&lt;p&gt;Claude Code usage on a Pro, Max, or Ultra plan is the plan allowance, shared with Claude on the web and on the desktop, and you read it with &lt;code&gt;/usage&lt;/code&gt; or in Settings, under Usage. Anthropic documents that in its &lt;a href="https://support.claude.com/en/articles/11145838-use-claude-code-with-your-pro-max-or-ultra-plan" rel="noopener noreferrer"&gt;help center&lt;/a&gt; and in the &lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;costs guide&lt;/a&gt;. An Anthropic Console API key is a different billing path.&lt;/p&gt;

&lt;p&gt;OpenAI's &lt;a href="https://chatgpt.com/codex/pricing/" rel="noopener noreferrer"&gt;Codex pricing page&lt;/a&gt; tells you to check remaining limits in the Codex usage dashboard, or with &lt;code&gt;/status&lt;/code&gt; during a CLI session. Local sessions and cloud chats share the plan allowance. A usage-based API key is listed as a separate column. If a tracker required that key, it would be tempting to assume it was reading the same pool you care about. It might not be.&lt;/p&gt;

&lt;p&gt;Cursor is explicit about the same split. Its &lt;a href="https://cursor.com/help/models-and-usage/usage-limits" rel="noopener noreferrer"&gt;usage documentation&lt;/a&gt; says that on individual plans, requests made with your own API key do not count toward included usage. Your provider bills you directly. A meter that only knew how to call an API with a key you pasted would miss the weekly or monthly pool you were trying to watch.&lt;/p&gt;

&lt;p&gt;So "just paste your API key" is not a shortcut. It is often a request for the wrong credential, plus a new secret sitting in someone else's app. The better starting point is: you already signed in to the vendor. Use that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What reuse looks like in the menu bar
&lt;/h2&gt;

&lt;p&gt;When TokenBar is connected, the menu can name the source it is using. On a Mac where Codex is signed in, that line has read "Live process detected (Codex app) - Source: OAuth," next to the session and weekly bars and the time until reset. The useful part of that sentence is the source. The app is not asking you to type a key into a form. It is using a login that Codex already has.&lt;/p&gt;

&lt;p&gt;The same idea applies to the other advertised providers, with a per-provider source you can choose when more than one is available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude can follow a Claude Code login, or a signed-in claude.ai session, or the &lt;code&gt;claude&lt;/code&gt; CLI.&lt;/li&gt;
&lt;li&gt;Codex can follow the Codex CLI or app login, or a signed-in ChatGPT session.&lt;/li&gt;
&lt;li&gt;Cursor can follow the Cursor app or a signed-in browser session.&lt;/li&gt;
&lt;li&gt;Gemini can follow a Gemini CLI login.&lt;/li&gt;
&lt;li&gt;Copilot can follow a GitHub login. GitHub's own apps often use &lt;a href="https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow" rel="noopener noreferrer"&gt;device authorization&lt;/a&gt; for that kind of sign-in, so you approve it on GitHub rather than pasting a token by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not that every provider speaks one protocol. They do not. CodexBar's own provider list, which is much longer than TokenBar's advertised set, is a reminder of how many ways a usage number can be fetched: official APIs, OAuth, device flow, a browser session, CLI output, local files. The point is that the default should be "use the session you already created," and that the menu should say which source is in play.&lt;/p&gt;

&lt;p&gt;We do not treat unadvertised providers as part of this product story. The advertised set is Claude, Codex, Cursor, Gemini, and Copilot. If you need a longer list, CodexBar is the open-source baseline, it is free, and it is very good.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoffs you actually hit
&lt;/h2&gt;

&lt;p&gt;Reusing a login is cleaner on paper than it is in a menu bar that should stay quiet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expiry belongs to the vendor.&lt;/strong&gt; If an OAuth token has expired, the honest fix is to let the vendor's own app or CLI refresh it, then read again. Inventing a parallel login just so a meter can keep working is how you end up with a second, half-documented session and a confusing sign-out story. When something is stale, the message should tell you to sign in to the tool you already use, not to invent a TokenBar account for that provider. TokenBar does not need a separate Claude, Codex, or Cursor account of its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keychain prompts can become noise.&lt;/strong&gt; Credentials on a Mac often live in the Keychain. A status item that wakes, reads a token, and puts a Keychain dialog in your face every few minutes is worse than no meter. TokenBar has a setting to avoid those prompts, and it backs off for a while after a prompt is denied, so one "Deny" does not turn into a loop. That is a real tradeoff: less nagging, and a stretch of time where a denied or locked Keychain means the meter cannot refresh. Asking someone to unlock the login Keychain is the right error when that is actually the problem. It is the wrong thing to ask every time the menu opens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A browser session is a fallback, not a tutorial in moving cookies around.&lt;/strong&gt; Some providers can be read from a session you already have in a browser. That can unblock someone who uses the web app more than the CLI. It is also easy to document badly. We would rather tell you to choose a source in the app than publish a recipe for copying cookie headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading usage does not change usage.&lt;/strong&gt; This should go without saying, and it still needs saying. A meter is a view. Official plan limits, reset times, and what happens when you hit them still come from the vendor. TokenBar gives you no extra quota, and it does not replace Settings, &lt;code&gt;/usage&lt;/code&gt;, or &lt;code&gt;/status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The app is not a claim that nothing ever leaves the Mac.&lt;/strong&gt; License checks, checkout, and similar account traffic go to TokenBar's own service. Reusing a vendor login is a decision about how we read that vendor, not a promise that the utility has no network of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell anyone building a sidecar for a tool people already pay for
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prefer the login the user already completed.&lt;/strong&gt; If the number you want is on a subscription plan, an API key is often a different number.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Show the source.&lt;/strong&gt; "OAuth," "CLI," or "web session" in the UI saves a lot of guesswork when the number looks wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Let the vendor refresh its own tokens.&lt;/strong&gt; Your job is to notice that the session is stale and send the user back to the official sign-in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat Keychain as a scarce attention budget.&lt;/strong&gt; Prompt when you must, back off when the user says no, and do not poll in a way that makes the system ask again.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Say what you do not do.&lt;/strong&gt; No extra quota. No second provider account if you can help it. No implication that watching the meter is a privacy guarantee.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where TokenBar fits
&lt;/h2&gt;

&lt;p&gt;TokenBar runs on macOS 14 or later. Basic is $5 a month. Pro is $15 once, a lifetime license. You choose the plan in the app. It shows usage and reset times for the advertised providers, and it does not increase any of their allowances. If you want the open-source menu bar meter on its own, start with &lt;a href="https://github.com/steipete/CodexBar" rel="noopener noreferrer"&gt;CodexBar&lt;/a&gt;. If you want to look at TokenBar, it is at &lt;a href="https://tokenbar.site/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=reuse-logins" rel="noopener noreferrer"&gt;tokenbar.site&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When you connect a third-party utility to a provider you already pay for, do you want it to reuse the existing login, even if that means a Keychain prompt, or would you rather paste a dedicated key so the access is obvious and easy to revoke?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic, Use Claude Code with your Pro, Max, or Ultra plan: &lt;a href="https://support.claude.com/en/articles/11145838-use-claude-code-with-your-pro-max-or-ultra-plan" rel="noopener noreferrer"&gt;https://support.claude.com/en/articles/11145838-use-claude-code-with-your-pro-max-or-ultra-plan&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic, Manage costs effectively: &lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;https://code.claude.com/docs/en/costs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI, Codex Pricing: &lt;a href="https://chatgpt.com/codex/pricing/" rel="noopener noreferrer"&gt;https://chatgpt.com/codex/pricing/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cursor, Usage and limits: &lt;a href="https://cursor.com/help/models-and-usage/usage-limits" rel="noopener noreferrer"&gt;https://cursor.com/help/models-and-usage/usage-limits&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub, Authorizing OAuth apps (device flow): &lt;a href="https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow" rel="noopener noreferrer"&gt;https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CodexBar (MIT), Peter Steinberger: &lt;a href="https://github.com/steipete/CodexBar" rel="noopener noreferrer"&gt;https://github.com/steipete/CodexBar&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pixel Agents (MIT): &lt;a href="https://github.com/pixel-agents-hq/pixel-agents" rel="noopener noreferrer"&gt;https://github.com/pixel-agents-hq/pixel-agents&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>macos</category>
      <category>openai</category>
      <category>security</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why a Mac Writing Helper Needs Two Permissions, and Why It Still Refuses Password Fields</title>
      <dc:creator>E Daugherty LLC</dc:creator>
      <pubDate>Fri, 13 Nov 2026 16:10:01 +0000</pubDate>
      <link>https://dev.to/jeff_feldman_7ab3898/why-a-mac-writing-helper-needs-two-permissions-and-why-it-still-refuses-password-fields-19ia</link>
      <guid>https://dev.to/jeff_feldman_7ab3898/why-a-mac-writing-helper-needs-two-permissions-and-why-it-still-refuses-password-fields-19ia</guid>
      <description>&lt;p&gt;A writing helper that works inside someone else's text field has to ask macOS for a lot of trust. Accessibility permission is how an app reads and, if you approve it, replaces text in another app. Input Monitoring is how it can see keyboard events while you are in Mail, Notes, or a browser. Those are two different Privacy &amp;amp; Security toggles, and they should be. People are right to pause on both.&lt;/p&gt;

&lt;p&gt;We build DraftKey, a Mac app for Tab autocomplete and rewrites you review before they are inserted. This post is about the permission model behind a helper like that: why two system permissions are required, how we ask for them, and the cases we refuse even after you have said yes. The general mechanics come from Apple's public documentation, linked at the end. The product rules come from the shipped app's own wording, including the Apps &amp;amp; privacy section in Settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two panes, two jobs
&lt;/h2&gt;

&lt;p&gt;Apple documents these as separate resources, and System Settings treats them that way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://support.apple.com/guide/mac-help/allow-accessibility-apps-to-access-your-mac-mh43185/mac" rel="noopener noreferrer"&gt;Accessibility&lt;/a&gt; is under Privacy &amp;amp; Security. It is the permission that lets an app control your Mac on your behalf: read user interface elements, including text, and change them. A Mac app can ask whether it is a trusted Accessibility client with &lt;code&gt;AXIsProcessTrustedWithOptions&lt;/code&gt;. If the process is not trusted, it cannot usefully work in another app's field. The honest usage description is therefore about suggestions and about replacing text only when the user approves, not about "making writing nicer" in the abstract.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://support.apple.com/guide/mac-help/control-access-to-input-monitoring-on-mac-mchl4cedafb6/mac" rel="noopener noreferrer"&gt;Input Monitoring&lt;/a&gt; is a different list. Apple's own support page is blunt about what it means: some apps can monitor your keyboard, mouse, or trackpad even when you are using other apps. You turn that ability on or off per app. Apple's documentation for resetting TCC access even uses a different service name for it, &lt;code&gt;ListenEvent&lt;/code&gt;, alongside a separate &lt;code&gt;PostEvent&lt;/code&gt; service for sending keystrokes and a separate Accessibility service. If you have ever wondered why a shortcut utility and a window manager can end up in different panes, that split is why.&lt;/p&gt;

&lt;p&gt;DraftKey needs both, for different reasons. Reading a compatible text field, and writing a rewrite back after you press Insert, is an Accessibility job. Noticing Tab when a suggestion is showing, or Command-Option when you want Writing tools, is a keyboard job. Typing sounds, which are a separate feature you can turn off, also need keyboard access. The in-app prompts say so directly: "Allow Input Monitoring for shortcuts and typing sounds," and "Allow Accessibility and keyboard access in DraftKey."&lt;/p&gt;

&lt;p&gt;Lumping those into one explanation is how permission dialogs start to sound like a blank check. Keeping them separate is more work in onboarding, and it is clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask, then get out of the way
&lt;/h2&gt;

&lt;p&gt;The system already has a prompt and a Settings pane for each permission. The useful work in the app is not inventing a third one. It is sending people to the right pane, then noticing when they come back.&lt;/p&gt;

&lt;p&gt;DraftKey's onboarding copy is: "Approve DraftKey in the macOS prompt or System Settings, then return here." Settings also has a Refresh permissions action. That sounds minor until you remember how often a Mac permission is granted to one build, then seems to vanish after a move, a rename, or a reinstall. Apple publishes a &lt;a href="https://developer.apple.com/documentation/xcode/resetting-access-to-protected-resources-in-macos" rel="noopener noreferrer"&gt;&lt;code&gt;tccutil&lt;/code&gt;-based reset procedure&lt;/a&gt; for developers for exactly this class of problem. Users should not have to learn that command. They should have a button that re-checks trust and, if needed, opens the same Privacy &amp;amp; Security pane again.&lt;/p&gt;

&lt;p&gt;A good permission ask is specific about what you will and will not do. "Allow access to your keyboard and supported text fields" is narrower than "allow this app to control your Mac," and it is closer to the truth. It also leaves room for the next section, which matters more than the wording of the dialog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permission granted is not permission everywhere
&lt;/h2&gt;

&lt;p&gt;Once both system toggles are on, the dangerous default is "every text field, every app." We do not take that default.&lt;/p&gt;

&lt;p&gt;Settings includes an Apps &amp;amp; privacy section whose subtitle is "Choose where DraftKey works." You can turn writing help off for an individual app. If you invoke it in a place that is turned off, the app tells you: "Writing help is turned off for this app. Review Apps &amp;amp; permissions in DraftKey." That is an allowlist you can edit, not a one-time consent that silently covers the whole machine.&lt;/p&gt;

&lt;p&gt;Some exclusions are not optional, and they are not buried.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Password fields are always excluded.&lt;/li&gt;
&lt;li&gt;Password managers always exclude writing help and typing sounds.&lt;/li&gt;
&lt;li&gt;Terminals start with sounds only. Writing help is off there until you change it.&lt;/li&gt;
&lt;li&gt;When secure input is active, writing help is unavailable. The message is: "Writing help is unavailable while secure input is active. Click a normal text field and try again."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last two points are worth unpacking, because they are easy to misread as bugs.&lt;/p&gt;

&lt;p&gt;Apple's &lt;a href="https://developer.apple.com/documentation/appkit/nssecuretextfield" rel="noopener noreferrer"&gt;&lt;code&gt;NSSecureTextField&lt;/code&gt;&lt;/a&gt; is the standard password field. It displays bullets instead of characters and does not allow cutting or copying its contents. A writing helper that suggested the next word in a password field, or offered to "rewrite" it, would be doing the wrong thing even if the user had granted Accessibility. So the rule is not "skip password managers we recognize." It is "skip password fields, wherever they appear."&lt;/p&gt;

&lt;p&gt;Secure input is broader than a single field. When a Mac app enables secure keyboard entry, the system is telling other processes to stop observing those keystrokes. Apple documented the fair use of this mechanism years ago in &lt;a href="https://developer.apple.com/library/archive/technotes/tn2150/_index.html" rel="noopener noreferrer"&gt;Technical Note TN2150, Using Secure Event Input Fairly&lt;/a&gt;: turn it on only while sensitive input is happening, and turn it off afterward, because leaving it on breaks other software. From the helper's point of view, the only correct behavior while secure input is on is to refuse. If a password prompt is in front, or a terminal has Secure Keyboard Entry enabled, DraftKey should not try to be helpful. It should wait until you click a normal text field.&lt;/p&gt;

&lt;p&gt;Terminals are a softer default, not a hard ban. A terminal looks like a text field, and some of what people type there is just a commit message. A lot of it is not: commands, tokens, remote sessions. Starting with sounds only, and leaving writing help off, means the app is present without reading shell input until you opt in for that specific app. You can still enable it if your actual use is writing prose in that window. The default assumes you might not be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compatible fields, not every app
&lt;/h2&gt;

&lt;p&gt;Even in apps you have allowed, support depends on how the editor exposes its text. Some custom editors do not. DraftKey is a helper for compatible text fields, not a claim that every Mac app will work. That limit belongs next to the permission talk, because a user who has just opened Accessibility and Input Monitoring deserves an accurate picture of what those permissions buy.&lt;/p&gt;

&lt;p&gt;The same honesty applies to what the app does after you grant them. Tab autocomplete suggests the rest of a sentence and adds it only when you accept it. A rewrite opens a preview, and nothing is written back until you choose Insert. Those are separate actions, and both are opt-in at the moment of use. Granting a system permission is not the same thing as accepting a change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell anyone asking for these permissions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat Accessibility and Input Monitoring as different asks.&lt;/strong&gt; They live in different panes because they do different things. Explain each one in a sentence, not as a bundle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Send people to System Settings, then detect the return.&lt;/strong&gt; A custom primer screen cannot grant the permission for them. A refresh action can close the loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do not let system consent become app-wide access.&lt;/strong&gt; Give people an apps list. Default sensitive classes off, or exclude them entirely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refuse password fields and secure input, even if the APIs would let you try.&lt;/strong&gt; If your feature is writing help, those contexts are not writing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Say "compatible text fields" and mean it.&lt;/strong&gt; A permission grant does not make an inaccessible editor accessible.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where DraftKey fits
&lt;/h2&gt;

&lt;p&gt;If you want to try the result, DraftKey needs an Apple Silicon Mac on macOS 14 or later, and its writing features need at least 8 GB of memory and a separate writing-model download of about 1.12 GB, with at least 2.2 GB of free space. Every Mac gets 500 free words of writing help. After that it is $4.99 once, for up to three Macs you own. It works in compatible text fields, password fields are excluded, and some custom editors are not supported. You can &lt;a href="https://getdraftkey.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=two-permissions" rel="noopener noreferrer"&gt;read more about it here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you ship a Mac app that needs Accessibility or Input Monitoring, how do you explain the ask without making it sound like a blank check? And which apps did you default to off?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apple Support, Allow accessibility apps to access your Mac: &lt;a href="https://support.apple.com/guide/mac-help/allow-accessibility-apps-to-access-your-mac-mh43185/mac" rel="noopener noreferrer"&gt;https://support.apple.com/guide/mac-help/allow-accessibility-apps-to-access-your-mac-mh43185/mac&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple Support, Control access to input monitoring on Mac: &lt;a href="https://support.apple.com/guide/mac-help/control-access-to-input-monitoring-on-mac-mchl4cedafb6/mac" rel="noopener noreferrer"&gt;https://support.apple.com/guide/mac-help/control-access-to-input-monitoring-on-mac-mchl4cedafb6/mac&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple, Resetting access to protected resources in macOS: &lt;a href="https://developer.apple.com/documentation/xcode/resetting-access-to-protected-resources-in-macos" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/xcode/resetting-access-to-protected-resources-in-macos&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple, &lt;code&gt;NSSecureTextField&lt;/code&gt;: &lt;a href="https://developer.apple.com/documentation/appkit/nssecuretextfield" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/appkit/nssecuretextfield&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple, Technical Note TN2150, Using Secure Event Input Fairly: &lt;a href="https://developer.apple.com/library/archive/technotes/tn2150/_index.html" rel="noopener noreferrer"&gt;https://developer.apple.com/library/archive/technotes/tn2150/_index.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DraftKey public release notes: &lt;a href="https://github.com/auratechnologiesllc/DraftKey/releases" rel="noopener noreferrer"&gt;https://github.com/auratechnologiesllc/DraftKey/releases&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>security</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Shipping 52 Releases of a Mac App: What Signed Updates Have to Get Right</title>
      <dc:creator>E Daugherty LLC</dc:creator>
      <pubDate>Fri, 13 Nov 2026 14:52:36 +0000</pubDate>
      <link>https://dev.to/jeff_feldman_7ab3898/shipping-52-releases-of-a-mac-app-what-signed-updates-have-to-get-right-2ne</link>
      <guid>https://dev.to/jeff_feldman_7ab3898/shipping-52-releases-of-a-mac-app-what-signed-updates-have-to-get-right-2ne</guid>
      <description>&lt;p&gt;Your first auto-update is a promise. Your second one is where you find out whether you kept it.&lt;/p&gt;

&lt;p&gt;We build DraftKey, a small Mac app for Tab autocomplete and reviewed rewrites. Between September 20 and November 11, 2026 we tagged 52 public releases on GitHub, from 0.1.1 to 0.1.62. That is a lot of updates to push at people who installed a writing tool and mostly want it to stay out of the way. This post is about the boring plumbing that made that pace survivable, and the one early bug that forced users to reinstall by hand.&lt;/p&gt;

&lt;p&gt;Everything about DraftKey below comes from our public release notes. The general mechanics come from Apple's and the Sparkle project's public documentation, linked at the end. If you ship a Mac app outside the App Store, most of this applies to you whatever updater you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release 0.1.1: decide what an update must never touch
&lt;/h2&gt;

&lt;p&gt;The 0.1.1 notes are short, and every line is a rule we still follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can check for and install signed updates from the menu bar or from Settings, under About.&lt;/li&gt;
&lt;li&gt;Automatic update checks can be turned off.&lt;/li&gt;
&lt;li&gt;Installing an update requires your approval.&lt;/li&gt;
&lt;li&gt;Updates preserve your license, your preferences, and the downloaded writing model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last line matters more than it looks. DraftKey's download is about 1.12 GB and needs about 2.2 GB of free space, because it ships with a writing model. If an update quietly dropped that model, or reset the license, every release would feel like a reinstall. So before shipping a second version, write down the state that has to survive an update. For us it was three things: the license, the preferences, and the model. Then keep that state outside the app bundle, because the bundle is exactly what gets replaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release 0.1.2: the updater bug you can't update your way out of
&lt;/h2&gt;

&lt;p&gt;The very next release, the same day, says this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fix Check for Updates and automatic update checks failing to start. Verify signed updates before extracting them, as required for signed feeds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then the uncomfortable part:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you installed 0.1.0 or 0.1.1, download this installer once and replace DraftKey in Applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the trap. When the updater itself is broken, the fix can't reach anyone through the updater. Everyone on the broken build has to come back to your website and reinstall manually, and most of them won't know they need to.&lt;/p&gt;

&lt;p&gt;Two lessons came out of that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the update path, not just the app.&lt;/strong&gt; Before a release goes out, install the previous public build on a clean machine or user account and update it to the new one through the real feed. Launching the new build on its own tells you nothing about whether people can get to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify before you unpack.&lt;/strong&gt; Sparkle's documentation describes the model most Mac updaters follow. You generate a key pair once with its &lt;code&gt;generate_keys&lt;/code&gt; tool, which keeps the private key in your Keychain. You ship the public key inside the app, sign each update archive with &lt;code&gt;sign_update&lt;/code&gt;, and the app refuses any archive whose signature doesn't match. The order matters. Check the signature first and only then extract, so a tampered or corrupted archive never gets unpacked into a temporary folder at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing and notarization are two different checks
&lt;/h2&gt;

&lt;p&gt;People mix these up, so here is the short version from Apple's documentation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code signing with a Developer ID&lt;/strong&gt; says who built the app and that it hasn't changed since it was signed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notarization&lt;/strong&gt; means you upload the signed build to Apple's notary service, which runs automated checks and returns a ticket. Apple requires the hardened runtime for notarized apps. You submit with &lt;code&gt;xcrun notarytool submit&lt;/code&gt; and attach the ticket to the disk image or app with &lt;code&gt;xcrun stapler staple&lt;/code&gt;, so Gatekeeper can verify it even when the Mac is offline at first launch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update signing&lt;/strong&gt;, like Sparkle's EdDSA signature, is separate again. It protects the update feed, not first launch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You want all three. Our 0.1.56 and 0.1.57 notes both lead with the same sentence, "Freshly built, signed, and notarized installer," and both open the standard drag-to-Applications window. A boring, familiar first install is a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask before installing, and let people turn it off
&lt;/h2&gt;

&lt;p&gt;It is tempting to update silently. We went the other way on purpose. Checks can be disabled, and an install waits for your approval. For a tool that sits in your text fields all day, the update should never be the thing that interrupts a sentence.&lt;/p&gt;

&lt;p&gt;Making updates visible also helped in ways we didn't plan for. Release 0.1.11 added an update check right inside onboarding, along with a persistent update status. Someone who downloads an older installer from a cached link gets told right away that a newer build exists, before they form an opinion from stale code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write release notes for the person, not the commit log
&lt;/h2&gt;

&lt;p&gt;Look at almost any of the 52 notes and you will see the same shape: what changed for you, in plain words, and occasionally what you have to do. Release 0.1.33 says checks "resume after memory pressure clears." Release 0.1.60 says the Open DraftKey button on the website "now starts guided setup, the same as opening it from Applications." There is no "refactored the state machine" in there.&lt;/p&gt;

&lt;p&gt;When the notes are readable, they can do two more jobs. They tell users when an update is worth installing, and later they give you an honest public record. This post leans on that record instead of memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist if you ship Mac updates yourself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;List the state an update must preserve, and keep it outside the app bundle.&lt;/li&gt;
&lt;li&gt;Ship your update signing public key in the very first public build. Adding it later means one more manual reinstall for everyone.&lt;/li&gt;
&lt;li&gt;Verify the signature before extracting anything.&lt;/li&gt;
&lt;li&gt;Before every release, update the previous public build through the real feed.&lt;/li&gt;
&lt;li&gt;Sign with Developer ID, enable the hardened runtime, notarize, and staple the installer.&lt;/li&gt;
&lt;li&gt;Ask before installing, and let people turn automatic checks off.&lt;/li&gt;
&lt;li&gt;Treat an updater bug as a special incident: put the manual reinstall instructions in the release notes and on the download page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to see how this feels from the user side, DraftKey is at &lt;a href="https://getdraftkey.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mac_signed_updates" rel="noopener noreferrer"&gt;getdraftkey.com&lt;/a&gt;. It needs an Apple Silicon Mac on macOS 14 or later with 8 GB of RAM.&lt;/p&gt;

&lt;p&gt;What broke the first time you shipped an updater? I'm especially curious whether anyone found a clean way to rescue users stuck on a build with a broken update check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DraftKey public release notes: &lt;a href="https://github.com/auratechnologiesllc/DraftKey/releases" rel="noopener noreferrer"&gt;https://github.com/auratechnologiesllc/DraftKey/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sparkle documentation: &lt;a href="https://sparkle-project.org/documentation/" rel="noopener noreferrer"&gt;https://sparkle-project.org/documentation/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple, Notarizing macOS software before distribution: &lt;a href="https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple, Hardened Runtime: &lt;a href="https://developer.apple.com/documentation/security/hardened-runtime" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/security/hardened-runtime&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>macos</category>
      <category>swift</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How a Menu Bar App Reconstructs Codex and Claude Code Usage Limits</title>
      <dc:creator>E Daugherty LLC</dc:creator>
      <pubDate>Fri, 13 Nov 2026 02:02:20 +0000</pubDate>
      <link>https://dev.to/jeff_feldman_7ab3898/how-a-menu-bar-app-reconstructs-codex-and-claude-code-usage-limits-9ba</link>
      <guid>https://dev.to/jeff_feldman_7ab3898/how-a-menu-bar-app-reconstructs-codex-and-claude-code-usage-limits-9ba</guid>
      <description>&lt;p&gt;If you have typed &lt;code&gt;/status&lt;/code&gt; in Codex, or &lt;code&gt;/usage&lt;/code&gt; in Claude Code, and then opened Settings → Usage to double-check, you already know the awkward part. The remaining allowance is not one number. It is at least two windows, they do not always reset together, and official docs point you at more than one place to read them.&lt;/p&gt;

&lt;p&gt;A macOS menu bar meter is a small UI on top of that problem. The interesting work is reconstructing a trustworthy reading from a session the provider already gave you, then deciding what to show when the reading is incomplete, stale, or about to change. This post is about that plumbing. I'm citing official provider docs for how the limits work, and the public docs of &lt;a href="https://github.com/steipete/CodexBar" rel="noopener noreferrer"&gt;CodexBar&lt;/a&gt;, Peter Steinberger's MIT-licensed macOS 14+ menu bar app, as an open-source reference for how a meter fetches them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limit is a set of windows, not a tank
&lt;/h2&gt;

&lt;p&gt;OpenAI's help center is explicit about Codex and ChatGPT Work on plans that have both constraints: usage limits "may apply over a five-hour window and a weekly window," and "where both apply, you need allowance remaining in both to continue." A new five-hour window starts with your first message after the previous window ends, and you can fill it "before five hours have passed, even if you still have weekly usage remaining." Check remaining allowance and reset times in Settings → Usage. The percentage is not a message count either. Your allowance "does not correspond to a fixed number of messages or tasks." How far it goes depends on the model, the task, and the settings. (&lt;a href="https://help.openai.com/en/articles/20001516-managing-usage-with-gpt-6-astra-in-work-and-codex" rel="noopener noreferrer"&gt;Managing usage with GPT-6 Astra in Work and Codex&lt;/a&gt;) In the Codex CLI, &lt;code&gt;/status&lt;/code&gt; shows "the chat ID, context usage, and rate limits." (&lt;a href="https://learn.chatgpt.com/docs/developer-commands" rel="noopener noreferrer"&gt;Developer commands&lt;/a&gt;) OpenAI's resets guide also describes a &lt;code&gt;/usage&lt;/code&gt; command in the Codex CLI for redeeming a banked reset. (&lt;a href="https://help.openai.com/en/articles/20001498-how-resets-work-in-codex" rel="noopener noreferrer"&gt;How resets work in Codex&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Anthropic describes a similar shape for paid Claude plans: "a session limit that resets every five hours, and weekly limits that reset once every week." The five-hour period starts with your first message and "resets in full five hours later." Weekly limits reset at a fixed time each week assigned to your account. Usage on claude.ai, Claude Code, and Claude Desktop "counts towards the same usage limit." (&lt;a href="https://support.claude.com/en/articles/11647753-how-do-usage-and-length-limits-work" rel="noopener noreferrer"&gt;How do usage and length limits work?&lt;/a&gt;) In Claude Code, &lt;code&gt;/usage&lt;/code&gt; shows "session cost, plan usage limits, and activity stats." (&lt;a href="https://code.claude.com/docs/en/commands" rel="noopener noreferrer"&gt;Commands&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The windows are not universal. OpenAI currently says Pro 100, Pro 200, and Pro 500 have no five-hour usage limit in Work and Codex. Anthropic says verified Ultra accounts don't have a five-hour limit, and some plans also have a separate weekly limit for Fable. (&lt;a href="https://help.openai.com/en/articles/20001516-managing-usage-with-gpt-6-astra-in-work-and-codex" rel="noopener noreferrer"&gt;Astra usage&lt;/a&gt;, &lt;a href="https://support.claude.com/en/articles/11145838-use-claude-code-with-your-pro-max-or-ultra-plan" rel="noopener noreferrer"&gt;Claude Code on Pro, Max, or Ultra&lt;/a&gt;) A meter that assumes every account has a five-hour bar will invent a window the provider does not report.&lt;/p&gt;

&lt;p&gt;Reset times can also move. OpenAI documents banked, automatic, and purchased resets that restore covered allowances and can change the next refresh time without changing the billing date. On plans with both limits, "the same usage counts against the 5-hour and weekly allowances, but those periods can end at different times." Its example is a weekly period ending at noon while a five-hour period continues until 3 p.m., leaving you with a full weekly window and almost no session window. (&lt;a href="https://help.openai.com/en/articles/20001498-how-resets-work-in-codex" rel="noopener noreferrer"&gt;How resets work in Codex&lt;/a&gt;) The honest display is "this window, this percentage, this reset time," not a single "you are fine until Friday."&lt;/p&gt;

&lt;p&gt;The shape changes over time, too. As of mid-November 2026, OpenAI's help center says that starting in December, Plus, Pro, and Business usage will be shared across ChatGPT and Codex, measured in tokens, with remaining usage and reset times in Settings → Usage. Instant chat will not draw from that usage. (&lt;a href="https://help.openai.com/en/articles/20001625-understanding-chatgpt-and-codex-usage-limits" rel="noopener noreferrer"&gt;Understanding ChatGPT and Codex usage limits&lt;/a&gt;) A meter has to read what the provider reports now, not a hardcoded plan table. Context-window length is also a different kind of limit from usage: hitting a session cap means waiting or changing plan, while hitting a length limit means the current conversation is too large. Those should not be the same bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part is the data source
&lt;/h2&gt;

&lt;p&gt;None of that official data arrives as a single public "menu bar API." CodexBar's authoring guide describes the practical response: each provider is a descriptor plus a pipeline of fetch strategies (CLI, OAuth, API token, browser cookies, a local probe, a web dashboard), tried in priority order, with fallback when a strategy is unavailable. (&lt;a href="https://github.com/steipete/CodexBar/blob/main/docs/provider.md" rel="noopener noreferrer"&gt;Provider authoring guide&lt;/a&gt;) As of November 12, 2026, its provider overview says it registers 113 provider IDs, in part because one company can expose more than one surface with a different auth source and quota shape. (&lt;a href="https://github.com/steipete/CodexBar/blob/main/docs/providers.md" rel="noopener noreferrer"&gt;providers.md&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The data flow is straightforward: a background refresh runs provider probes, writes a snapshot into a store, and the menu, icon, and widgets read from that store. (&lt;a href="https://github.com/steipete/CodexBar/blob/main/docs/architecture.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt;) The interesting policy is how each probe gets a session without asking for a password. CodexBar's README puts it as reusing existing provider sessions (OAuth, device flow, API keys, browser cookies, local files) so "no passwords are stored."&lt;/p&gt;

&lt;p&gt;For Codex, the documented Auto order is a personal access token if one is available, then the OAuth API using credentials already on disk at &lt;code&gt;~/.codex/auth.json&lt;/code&gt; (or &lt;code&gt;$CODEX_HOME/auth.json&lt;/code&gt;), then a local CLI RPC server started as &lt;code&gt;codex app-server&lt;/code&gt;, which exposes &lt;code&gt;account/read&lt;/code&gt; and &lt;code&gt;account/rateLimits/read&lt;/code&gt;. The app maps &lt;code&gt;primary_window&lt;/code&gt; and &lt;code&gt;secondary_window&lt;/code&gt; onto the session and weekly lanes. An optional OpenAI web dashboard path is off by default and is not an Auto fallback. Parsing the rendered &lt;code&gt;/status&lt;/code&gt; panel in a Codex TUI is a manual diagnostic only, because a bare &lt;code&gt;codex&lt;/code&gt; TUI "can start interactive auth and open browser tabs." Automatic refresh does not take that path. (&lt;a href="https://github.com/steipete/CodexBar/blob/main/docs/codex.md" rel="noopener noreferrer"&gt;docs/codex.md&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;For Claude, the documented app Auto order is an OAuth usage API, then a Claude CLI probe, then a browser-cookie web API. The OAuth path reuses Claude Code credentials and maps &lt;code&gt;five_hour&lt;/code&gt; to the session window and &lt;code&gt;seven_day&lt;/code&gt; to the weekly window, only when those fields actually report utilization. The CLI fallback runs &lt;code&gt;claude&lt;/code&gt; in a dedicated probe directory, sends &lt;code&gt;/usage&lt;/code&gt;, and parses the "Current session" and "Current week" headers. A working Claude Code login or a signed-in browser tab does not, by itself, mean the meter can read that session. Some Education and organization-managed subscriptions return only a notice, with no numeric quota fields. In that case the docs say to report those limits as unavailable and never derive a percentage from spend or token totals. (&lt;a href="https://github.com/steipete/CodexBar/blob/main/docs/claude.md" rel="noopener noreferrer"&gt;docs/claude.md&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;That last rule is the one I would keep even if everything else changed. If the provider did not give you a quota, you do not get to invent one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quota is not the same as cost
&lt;/h2&gt;

&lt;p&gt;A second easy lie is turning a remaining-quota percentage into a dollar amount. CodexBar's docs draw the line directly: "Account quota and estimated API cost have different coverage." Remote quota can include work with no readable local token record, and "CodexBar cannot convert a quota percentage into dollars." Local cost scans, where they exist, read known session logs. The Settings → Usage &amp;amp; Spend view is "a local estimated-cost history page, not a billing receipt and not the menu-bar quota card." Amounts are list-price equivalents unless a source also reports plan-metered spend, and the app "never adds or ranks amounts across currencies." (&lt;a href="https://github.com/steipete/CodexBar/blob/main/docs/codex.md" rel="noopener noreferrer"&gt;docs/codex.md&lt;/a&gt;, &lt;a href="https://github.com/steipete/CodexBar/blob/main/docs/providers.md" rel="noopener noreferrer"&gt;providers.md&lt;/a&gt;) Keep those as two separate numbers, and say on screen which one the provider confirmed and which one is a local estimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How often to ask, and what to show when the answer is old
&lt;/h2&gt;

&lt;p&gt;Limits move while you work, but polling every few seconds is a bad deal for battery, for rate limits, and for any provider that treats a usage fetch as a real request. CodexBar's refresh loop makes the tradeoff visible. Fresh installs default to Adaptive cadence, a function of recent menu interaction, Low Power Mode, thermal state, and, only in an opt-in "agent-aware" mode, recent local coding activity. Every decision lands between 2 and 30 minutes. Manual refresh is always available. Transient network failures keep the last successful snapshot and its original measurement time; authentication failures and account changes still invalidate it. Stale or error states dim the icon instead of pretending the number is fresh. (&lt;a href="https://github.com/steipete/CodexBar/blob/main/docs/refresh-loop.md" rel="noopener noreferrer"&gt;docs/refresh-loop.md&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Two other documented choices are worth copying. CodexBar does not redeem or modify Codex reset credits; it can list them and optionally warn before they expire, but the provider remains in charge of the allowance. And the permissions it asks for are narrow: optional Full Disk Access only to read Safari cookies or local storage, Keychain access when you import a Chromium session or repair Claude OAuth, and Files &amp;amp; Folders prompts when a launched CLI needs a working directory. It does not request Screen Recording or Accessibility in the background, and it does not store passwords. (&lt;a href="https://github.com/steipete/CodexBar/blob/main/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt;) A stale percentage with a capture time is more honest than a blank icon or a guessed refill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where TokenBar fits
&lt;/h2&gt;

&lt;p&gt;We build &lt;a href="https://tokenbar.site/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=usage-limits-deep-dive" rel="noopener noreferrer"&gt;TokenBar&lt;/a&gt;, a macOS 14+ menu bar app at Aura Technologies. It keeps AI usage, credits, costs, and reset windows visible across supported providers, and it is built on CodexBar. The public trust page states that the app's repository history descends from Peter Steinberger's MIT-licensed project and includes later contributor work as well as our changes. TokenBar also includes a Pixel Agents panel, powered by a local installation of &lt;a href="https://github.com/pixel-agents-hq/pixel-agents" rel="noopener noreferrer"&gt;Pixel Agents&lt;/a&gt;, a separate MIT-licensed project.&lt;/p&gt;

&lt;p&gt;The site currently features Claude, Codex, Cursor, Gemini, and Copilot, and exact coverage varies by integration. Basic is $5 a month per Mac. Pro is $15 once, a lifetime license for one Mac. You download the app and choose a plan inside it. Provider subscriptions and API charges stay separate: TokenBar monitors connected tools, and it does not give you extra quota. If you only want limits and reset times in the menu bar, CodexBar is free, open source, and documents a much longer provider list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell anyone building a meter
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Model the windows the provider reports.&lt;/strong&gt; Do not assume every plan has a five-hour session bar, and do not collapse session and weekly remaining into one "you are fine" state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep quota and cost apart.&lt;/strong&gt; A remaining percentage is not an invoice, and a local log scan is not a substitute for missing remote token records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse a session the user already has.&lt;/strong&gt; OAuth files, a local CLI, or an opt-in browser session are enough to start. Store no passwords.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer a stale snapshot with a timestamp over a blank, and over a fabricated number.&lt;/strong&gt; If the provider returned no quota, show the limit as unavailable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have built anything that reads these allowances, I'm curious about a narrower question than "which API is nicest." When the CLI and the website disagree on remaining quota, which one do you treat as source of truth, and how stale do you let a menu bar reading get before you hide it?&lt;/p&gt;

</description>
      <category>macos</category>
      <category>openai</category>
      <category>claude</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
