<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Permissions on Nick Liu - Software Engineer</title>
    <link>https://nick-liu.com/tags/permissions/</link>
    <description>Recent content in Permissions on Nick Liu - Software Engineer</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <managingEditor>nickboy@users.noreply.github.com (Nick Liu)</managingEditor>
    <webMaster>nickboy@users.noreply.github.com (Nick Liu)</webMaster>
    <copyright>2026 Nick Liu</copyright>
    <lastBuildDate>Fri, 10 Jul 2026 20:05:23 -0700</lastBuildDate><atom:link href="https://nick-liu.com/tags/permissions/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Holding HTTP open for 590 seconds so a Stream Deck key can approve a tool call</title>
      <link>https://nick-liu.com/posts/permission-round-trip/</link>
      <pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate>
      <author>nickboy@users.noreply.github.com (Nick Liu)</author>
      <guid>https://nick-liu.com/posts/permission-round-trip/</guid>
      <description>&lt;div class=&#34;lead text-neutral-500 dark:text-neutral-400 !mb-9 text-xl&#34;&gt;&#xA;  &#xA;Claude Code wants to run a shell command. I want to press a physical Stream Deck key, the YES key two inches to the left of my keyboard, to approve it. The hook gets exactly one HTTP response to decide allow vs deny. The key press might land in 200 milliseconds; it might land seven minutes later, after I&#39;ve been pulled into a meeting and come back. The trick is that Claude Code&#39;s hook timeout is 600 seconds, which turns out to be just enough headroom to hold the HTTP response open the whole time and let a hardware button write the answer.&#xA;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;nav class=&#34;series-progress&#34; aria-label=&#34;Series progress&#34;&gt;&#xA;    &lt;span class=&#34;series-progress-label&#34;&gt;Building ClaudeDeck · Part 6 of 10&lt;/span&gt;&#xA;    &lt;ol class=&#34;series-progress-dots&#34;&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/claude-code-hooks-reality/&#34; title=&#34;The Claude Code hooks docs are wrong. Here&amp;#39;s what&amp;#39;s actually on the wire.&#34;&gt;1&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/streamdeck-sdk-quirks/&#34; title=&#34;Two Stream Deck SDK quirks that cost me a weekend&#34;&gt;2&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/lru-session-eviction/&#34; title=&#34;Five Stream Deck keys, N Claude sessions: LRU that keeps the order I see&#34;&gt;3&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/statusline-side-channel/&#34; title=&#34;The Claude Code statusline is a per-turn telemetry side channel&#34;&gt;4&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-done&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/plan-usage-statusline-pivot/&#34; title=&#34;I polled an undocumented endpoint for 18 hours. The data was on stdin.&#34;&gt;5&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot is-current&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/permission-round-trip/&#34; title=&#34;Holding HTTP open for 590 seconds so a Stream Deck key can approve a tool call&#34; aria-current=&#34;step&#34;&gt;6&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/pty-wrap-migration/&#34; title=&#34;I split my daemon in two so a Node subprocess could own the PTY&#34;&gt;7&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tcc-cdhash-trap/&#34; title=&#34;TCC pins your Accessibility grant to a cdhash. Every rebuild breaks it.&#34;&gt;8&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/tahoe-hotkey-dead-end/&#34; title=&#34;What replaced CGEventPost in my Stream Deck daemon&#34;&gt;9&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;li class=&#34;series-progress-dot&#34;&gt;&#xA;          &lt;a href=&#34;https://nick-liu.com/posts/launchd-bootstrap-debugging/&#34; title=&#34;launchctl unload returned 0. The daemon was still running. KeepAlive raced.&#34;&gt;10&lt;/a&gt;&#xA;        &lt;/li&gt;&lt;/ol&gt;&#xA;  &lt;/nav&gt;&#xA;&lt;div class=&#34;tested-with&#34; role=&#34;note&#34;&gt;&#xA;  &lt;span class=&#34;tested-with-icon&#34; aria-hidden=&#34;true&#34;&gt;🧪&lt;/span&gt;&#xA;  &lt;span&gt;Tested with &lt;strong&gt;Claude Code&lt;/strong&gt; &lt;code&gt;2.1.x&lt;/code&gt; · macOS&lt;/span&gt;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;p&gt;(Setup, for anyone who hasn&amp;rsquo;t seen this stack before: Claude Code is Anthropic&amp;rsquo;s terminal CLI for Claude, and one of its hook events, &lt;code&gt;PreToolUse&lt;/code&gt;, is a script Claude spawns and waits on before running a tool like &lt;code&gt;Bash&lt;/code&gt; or &lt;code&gt;Edit&lt;/code&gt;. The script&amp;rsquo;s stdout decides &amp;ldquo;allow&amp;rdquo; / &amp;ldquo;deny&amp;rdquo; / &amp;ldquo;ask&amp;rdquo;. Stream Deck is Elgato&amp;rsquo;s USB grid of programmable LCD keys. The plumbing I&amp;rsquo;m describing here lives in a daemon, a background process at &lt;code&gt;127.0.0.1:9127&lt;/code&gt;, that the hook script POSTs to and that the Stream Deck plugin connects to over WebSocket. For the hooks docs themselves and the four other gotchas in that layer, see &lt;a href=&#34;https://nick-liu.com/posts/claude-code-hooks-reality/&#34; &gt;the hooks-reality post&lt;/a&gt;.)&lt;/p&gt;</description>
      
    </item>
    
  </channel>
</rss>
