<?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: Marco Belini</title>
    <description>The latest articles on DEV Community by Marco Belini (@marcobelini).</description>
    <link>https://dev.to/marcobelini</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%2F4176097%2F210e945f-59ea-407b-9e97-7177a49e9869.png</url>
      <title>DEV Community: Marco Belini</title>
      <link>https://dev.to/marcobelini</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcobelini"/>
    <language>en</language>
    <item>
      <title>Four ways to hear from Codex on your Apple Watch (only one can answer)</title>
      <dc:creator>Marco Belini</dc:creator>
      <pubDate>Thu, 12 Nov 2026 16:28:27 +0000</pubDate>
      <link>https://dev.to/marcobelini/four-ways-to-hear-from-codex-on-your-apple-watch-only-one-can-answer-16li</link>
      <guid>https://dev.to/marcobelini/four-ways-to-hear-from-codex-on-your-apple-watch-only-one-can-answer-16li</guid>
      <description>&lt;p&gt;&lt;em&gt;I make Tapproval, one of the options below, so read this with that in mind. Facts as of 9 November 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Codex stops when it needs you: a command to allow, a file change to approve. If you have stepped away from the keyboard, the work waits until you come back.&lt;/p&gt;

&lt;p&gt;There are four ways to have your Apple Watch tell you. They differ in one thing that matters more than it sounds: whether the watch can only &lt;em&gt;tell&lt;/em&gt; you, or can also &lt;em&gt;answer&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Codex's own alerts, on the computer
&lt;/h2&gt;

&lt;p&gt;The Codex CLI can raise a terminal notification when it needs approval or finishes a turn. In &lt;code&gt;~/.codex/config.toml&lt;/code&gt;, the &lt;code&gt;[tui]&lt;/code&gt; table's &lt;code&gt;notifications&lt;/code&gt; setting turns them on and can limit them to events such as &lt;code&gt;approval-requested&lt;/code&gt; and &lt;code&gt;agent-turn-complete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They appear on the computer, through your terminal. They reach your wrist only if you are close enough to see the screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good fit if&lt;/strong&gt; you stay in the room and only need a nudge. Nothing to install.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A notify script that pings your phone
&lt;/h2&gt;

&lt;p&gt;Codex can also run a program of your choice, set with &lt;code&gt;notify&lt;/code&gt; in the same file. It hands the program one JSON argument describing the turn. A short script there can send a push through ntfy or Pushover, and your watch shows it like any other phone notification.&lt;/p&gt;

&lt;p&gt;The limit: Codex's configuration guide says &lt;code&gt;notify&lt;/code&gt; currently fires only for &lt;code&gt;agent-turn-complete&lt;/code&gt;. It tells you a turn is done, not that Codex is waiting for permission. For approvals, a script has to sit in a Codex hook instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The ChatGPT app on your iPhone
&lt;/h2&gt;

&lt;p&gt;OpenAI's remote connections let the ChatGPT app on a phone follow Codex on a computer, approve commands and get notified when a task completes or needs you. The computer runs the ChatGPT desktop app on macOS or Windows, signed in to the same account as the phone.&lt;/p&gt;

&lt;p&gt;Your watch shows the phone's notification the way it shows any iPhone app's. The approval itself is made on the phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good fit if&lt;/strong&gt; you use Codex through the ChatGPT desktop app and carry your phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A watch app that answers
&lt;/h2&gt;

&lt;p&gt;The fourth kind puts the question itself on the watch and sends your answer back. On the computer, a hook in Codex's &lt;code&gt;hooks.json&lt;/code&gt; hands each permission question to a small relay; the watch app shows it, and one tap answers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tapproval.thoughtfulsteward.org/" rel="noopener noreferrer"&gt;Tapproval&lt;/a&gt; works this way. Each question arrives marked with one of five risk tiers, worked out on your own computer, so a force-push to main shows as CRITICAL before you tap. You can also follow a Codex chat live and reply to it from the wrist. The helper on the computer is open source and uses only Python's standard library; there is no account and no server of mine in between. The first 100 answers are free, then $5 once. No subscription.&lt;/p&gt;

&lt;p&gt;Others exist: Agent Approve has a watch app and lists Codex among its agents, on a monthly subscription. My &lt;a href="https://tapproval.thoughtfulsteward.org/compare.html" rel="noopener noreferrer"&gt;comparison page&lt;/a&gt; sets them side by side, with the source for every claim.&lt;/p&gt;

&lt;p&gt;One thing none of these change: Codex's own approval settings still decide which actions ask at all. Tapproval approves nothing on its own; every card that reaches the watch waits for your tap.&lt;/p&gt;

&lt;h2&gt;
  
  
  When your iPhone is not with you
&lt;/h2&gt;

&lt;p&gt;This is where the four part ways. Apple sends an iPhone app's notifications to the watch only while the two are connected. Leave the phone at home and options 2 and 3 go quiet on the wrist; option 1 never left the computer.&lt;/p&gt;

&lt;p&gt;A watch app can reach your computer on its own over the internet, on Wi-Fi or cellular. That is the case I built for: walking the dog while an agent works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Codex alerts&lt;/th&gt;
&lt;th&gt;Notify script&lt;/th&gt;
&lt;th&gt;ChatGPT app&lt;/th&gt;
&lt;th&gt;Watch app&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tells you it needs approval&lt;/td&gt;
&lt;td&gt;Yes, on the computer&lt;/td&gt;
&lt;td&gt;No, turn finished only&lt;/td&gt;
&lt;td&gt;Yes, on the phone&lt;/td&gt;
&lt;td&gt;Yes, on the watch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer on the watch&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No, on the phone&lt;/td&gt;
&lt;td&gt;Yes, one tap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with the iPhone away&lt;/td&gt;
&lt;td&gt;On the computer only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needs&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;A script and a push service&lt;/td&gt;
&lt;td&gt;ChatGPT desktop app, same account&lt;/td&gt;
&lt;td&gt;The app and its helper&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Setting up the fourth one
&lt;/h2&gt;

&lt;p&gt;Five minutes, three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get Tapproval from the App Store: search for "Tapproval" on your watch or iPhone (it is a watch app; no iPhone app is needed).&lt;/li&gt;
&lt;li&gt;Install the helper on the computer. The one-line command is in the &lt;a href="https://tapproval.thoughtfulsteward.org/apple-watch-codex.html" rel="noopener noreferrer"&gt;Codex setup guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;codex&lt;/code&gt; once and trust the new hook. Codex runs a hook only after you approve it, and the helper never approves it for you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then open the app on the watch while both are on the same Wi-Fi. It finds the computer by itself.&lt;/p&gt;

&lt;p&gt;If you use Claude Code too, the same install covers both.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tapproval is an independent app, not affiliated with or endorsed by OpenAI, Anthropic or Apple.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codex</category>
      <category>openai</category>
      <category>applewatch</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your Claude Code hook can say "allow" and be ignored, without a single error</title>
      <dc:creator>Marco Belini</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:12:27 +0000</pubDate>
      <link>https://dev.to/marcobelini/your-claude-code-hook-can-say-allow-and-be-ignored-without-a-single-error-1b3f</link>
      <guid>https://dev.to/marcobelini/your-claude-code-hook-can-say-allow-and-be-ignored-without-a-single-error-1b3f</guid>
      <description>&lt;p&gt;You wrote a &lt;code&gt;PermissionRequest&lt;/code&gt; hook. It decides, it prints "allow", your log says "allowed", and Claude Code shows the ordinary prompt anyway. No error, no warning. It looks exactly like a hook that chose to leave the question to you.&lt;/p&gt;

&lt;p&gt;We shipped that bug. Every approval tapped on a remote device was thrown away, on every install, for weeks. Here is the one-line cause, and two habits that would have caught it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause: a string where Claude Code wants an object
&lt;/h2&gt;

&lt;p&gt;Older examples print the decision as a string:&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="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;"allow"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code validates a hook's output. A string fails validation, and a hook that fails validation is treated as &lt;strong&gt;non-blocking&lt;/strong&gt;: the permission flow carries on as if your hook had never answered. The CLI's own validator says what it wants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PermissionRequest decision must be {"behavior": "allow"} or {"behavior": "deny", "message": "..."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we checked, the published hook docs still showed the string form. Trust the validator.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to print instead
&lt;/h2&gt;

&lt;p&gt;Standard output carries the answer and nothing else. Logs go to standard error.&lt;/p&gt;

&lt;p&gt;Allow:&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;"hookSpecificOutput"&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="nl"&gt;"hookEventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PermissionRequest"&lt;/span&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"behavior"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&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;Deny, with a reason Claude reads and acts on:&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;"hookSpecificOutput"&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="nl"&gt;"hookEventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PermissionRequest"&lt;/span&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"behavior"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"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;"Not on main. Push to a branch."&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;Leave it to the person by sending no &lt;code&gt;decision&lt;/code&gt; at all:&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;"hookSpecificOutput"&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="nl"&gt;"hookEventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PermissionRequest"&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;h2&gt;
  
  
  Two habits that would have caught it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One function builds the output, and a test checks the exact bytes it prints.&lt;/strong&gt; Assert that &lt;code&gt;decision&lt;/code&gt; is an object with &lt;code&gt;behavior&lt;/code&gt;, and that a deny carries a &lt;code&gt;message&lt;/code&gt;. Test what you print, not what you meant to print.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove one real allow, end to end.&lt;/strong&gt; Create a marker file, ask a session to delete it, approve the delete through your hook, and check that the file is gone. If it is still there and the ordinary prompt appeared, your answer was ignored.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wiring, briefly
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, or in a plugin's &lt;code&gt;hooks/hooks.json&lt;/code&gt;:&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="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"PermissionRequest"&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="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="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="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;"python3 /path/to/your-hook.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;86460&lt;/span&gt;&lt;span class="p"&gt;}]}]}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timeout is in seconds. A hook that waits for a person needs a long one, or the question disappears while they are still reading it. The event arrives as JSON on standard input, with &lt;code&gt;tool_name&lt;/code&gt;, &lt;code&gt;tool_input&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt;, and &lt;code&gt;permission_suggestions&lt;/code&gt; when the prompt would offer "don't ask again".&lt;/p&gt;

&lt;h2&gt;
  
  
  Three more traps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail closed.&lt;/strong&gt; If the hook can't parse the event, or crashes partway, print the "leave it to the person" block. Never allow on a guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless runs skip the hook.&lt;/strong&gt; A &lt;code&gt;claude -p&lt;/code&gt; run has no prompt to show, so it never reaches a &lt;code&gt;PermissionRequest&lt;/code&gt; hook. Use &lt;code&gt;--permission-prompt-tool&lt;/code&gt; with an MCP tool there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A lingering prompt isn't your bug.&lt;/strong&gt; The Claude app on a phone can keep showing a prompt your hook already answered (&lt;a href="https://github.com/anthropics/claude-code/issues/32493" rel="noopener noreferrer"&gt;claude-code#32493&lt;/a&gt;). The command ran; the marker test settles it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Checked against Claude Code 2.1.280, September 2026.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://tapproval.thoughtfulsteward.org" rel="noopener noreferrer"&gt;Tapproval&lt;/a&gt;, an open-source &lt;code&gt;PermissionRequest&lt;/code&gt; hook in standard-library Python, with an Apple Watch app that answers these prompts from the wrist. Its tests check the exact JSON, because we learned this the hard way.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>python</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
