<?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: Clinton Obi</title>
    <description>The latest articles on DEV Community by Clinton Obi (@clinton_obi_02a82fabda7d3).</description>
    <link>https://dev.to/clinton_obi_02a82fabda7d3</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%2F4207678%2Fdb0d039f-5d9c-49ff-9bb1-1962684716d5.jpg</url>
      <title>DEV Community: Clinton Obi</title>
      <link>https://dev.to/clinton_obi_02a82fabda7d3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/clinton_obi_02a82fabda7d3"/>
    <language>en</language>
    <item>
      <title>The sofa veto: I approve films and agent commands with a thumb, and a force-push has to wait</title>
      <dc:creator>Clinton Obi</dc:creator>
      <pubDate>Mon, 26 Oct 2026 04:29:20 +0000</pubDate>
      <link>https://dev.to/clinton_obi_02a82fabda7d3/the-sofa-veto-i-approve-films-and-agent-commands-with-a-thumb-and-a-force-push-has-to-wait-1lc5</link>
      <guid>https://dev.to/clinton_obi_02a82fabda7d3/the-sofa-veto-i-approve-films-and-agent-commands-with-a-thumb-and-a-force-push-has-to-wait-1lc5</guid>
      <description>&lt;p&gt;This is a submission for the Hacktoberfest Open-Source AI Challenge: Week 3, Slow and Steady.&lt;/p&gt;

&lt;p&gt;I watch films from a sofa that already ate the remote. I also leave coding agents running while I do that. Both of those habits needed the same lazy rule: the machine may ask, and I answer with a thumb, and nothing dangerous gets a cheap flick.&lt;/p&gt;

&lt;h1&gt;
  
  
  What I Built
&lt;/h1&gt;

&lt;p&gt;Lazy Hands is a Chrome extension plus a tiny localhost CLI. The extension finds the main HTML5 video on whatever page I already opened, reads my hand and my face on this machine, and waits. Palm pauses. A swipe skips ten seconds. If I look away long enough, it recaps from captions when I come back. When something wants consent, the machine switches to &lt;code&gt;approval_pending&lt;/code&gt; and a hold ring fills. A thumb held to the end approves. A palm held to the end refuses. A timeout refuses. A twitch does nothing.&lt;/p&gt;

&lt;p&gt;The lazy payoff is that I can stay on the sofa for two jobs that used to make me lean forward. I can drive a film without finding the remote. I can let a coding agent wait on &lt;code&gt;lazyhands approve -- git push --force origin main&lt;/code&gt; without walking back to the keyboard. The command sits there. The overlay says CRITICAL and prints one plain line about overwriting remote history. I have to hold the thumb for 2.4 seconds. &lt;code&gt;echo ship-it&lt;/code&gt; still waits, but only 0.8 seconds, and the line says it looks like a read or print.&lt;/p&gt;

&lt;p&gt;I wanted approval to feel like a veto, not a settings wall. There is no auto-allow list. There is no "always run git". &lt;a href="https://github.com/tankxu/gesture-approve" rel="noopener noreferrer"&gt;GestureApprove&lt;/a&gt; on macOS already does thumbs-up for Claude Code, and it can skip safe commands with an optional local model. I went the other way. The model is allowed to explain the command in one sentence. It is not allowed to wave the command through. The heuristic owns the risk tier. Gemma or DistilBART may rewrite the sentence. They never change the hold, and they never run the shell.&lt;/p&gt;

&lt;p&gt;The Chrome extension is the real product surface. It is not locked to the bundled player. Load &lt;code&gt;extension/&lt;/code&gt; unpacked, wake the camera from the popup, and open YouTube, a &lt;code&gt;file://&lt;/code&gt; tab, Vimeo, or the generic journal page at &lt;code&gt;/watch&lt;/code&gt;. The content script scores HTML5 &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; nodes the way a person would: size, visibility, playing state, a few host class names. MediaPipe runs in an offscreen page so the tab only receives poses.&lt;/p&gt;

&lt;p&gt;The CLI is the same hold ring for a coding agent. &lt;code&gt;node bin/lazyhands.mjs approve -- &amp;lt;command&amp;gt;&lt;/code&gt; starts a WebSocket on &lt;code&gt;127.0.0.1:4537&lt;/code&gt; if it is not already up. The command is printed with its risk line. The extension or &lt;code&gt;/watch&lt;/code&gt; shows the card. A held thumb runs the argv. A held palm, or a timeout, prints &lt;code&gt;Refused&lt;/code&gt; and the process exits. I think a coding agent should wait. I do not think it should get a faster path because the model sounded calm.&lt;/p&gt;

&lt;p&gt;This is a new project. It is not &lt;a href="https://github.com/maximbilan/Couchmode" rel="noopener noreferrer"&gt;Couchmode&lt;/a&gt; by Maxim Bilan. Couchmode is a Fire TV ACP client. Lazy Hands is a browser gesture layer and a local approve gate.&lt;/p&gt;

&lt;h1&gt;
  
  
  Demo
&lt;/h1&gt;

&lt;p&gt;There is no hosted YouTube URL. The recordings are local files in the repo. &lt;code&gt;npm run demo&lt;/code&gt; and &lt;code&gt;npm run demo:any&lt;/code&gt; rebuild them with Playwright against the bundled Blender trailers. A live webcam take from a Mac was not recorded for this post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recordings&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Player flow (idle, palm, hold ring, miss card): &lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/demo/lazy-hands-demo.mp4" rel="noopener noreferrer"&gt;docs/demo/lazy-hands-demo.mp4&lt;/a&gt; · &lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/demo/lazy-hands-demo.webm" rel="noopener noreferrer"&gt;webm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Generic page plus a waiting command: &lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/demo/lazy-hands-any-page.mp4" rel="noopener noreferrer"&gt;docs/demo/lazy-hands-any-page.mp4&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stills&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/screenshots/idle-1280.png" rel="noopener noreferrer"&gt;Idle hint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/screenshots/gesture-1280.png" rel="noopener noreferrer"&gt;Palm gesture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/screenshots/approval-hold-1280.png" rel="noopener noreferrer"&gt;Approval mid-hold&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/screenshots/miss-card-1280.png" rel="noopener noreferrer"&gt;Miss card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/screenshots/generic-page-1280.png" rel="noopener noreferrer"&gt;Generic journal page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/screenshots/generic-gesture-1280.png" rel="noopener noreferrer"&gt;Gesture on that page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/screenshots/bridge-waiting-1280.png" rel="noopener noreferrer"&gt;Command waiting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/screenshots/bridge-approved-1280.png" rel="noopener noreferrer"&gt;Command approved&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local preview after &lt;code&gt;npm run dev&lt;/code&gt;: &lt;a href="http://127.0.0.1:4521" rel="noopener noreferrer"&gt;http://127.0.0.1:4521&lt;/a&gt; and &lt;a href="http://127.0.0.1:4521/watch" rel="noopener noreferrer"&gt;http://127.0.0.1:4521/watch&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Code
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/Clintobi/lazy-hands" rel="noopener noreferrer"&gt;https://github.com/Clintobi/lazy-hands&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Five minutes, from the README:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run ext:build
&lt;span class="c"&gt;# Chrome → chrome://extensions → Developer mode → Load unpacked → extension/&lt;/span&gt;
&lt;span class="c"&gt;# Popup → Wake the camera&lt;/span&gt;

node bin/lazyhands.mjs approve &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;ship-it
node bin/lazyhands.mjs approve &lt;span class="nt"&gt;--&lt;/span&gt; git push &lt;span class="nt"&gt;--force&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first ask is LOW and 0.8s. The second is CRITICAL and 2.4s. Both print a one-line local risk rating before anyone moves a thumb. Palm or timeout still refuses.&lt;/p&gt;

&lt;h1&gt;
  
  
  How I Built It
&lt;/h1&gt;

&lt;p&gt;MediaPipe Hands and BlazeFace start in the tab on the web demo. In the extension they run in an offscreen document so &lt;code&gt;getUserMedia&lt;/code&gt; is not fighting the page. Poses travel as messages. The camera picture never leaves the machine.&lt;/p&gt;

&lt;p&gt;The session has two modes on purpose. &lt;code&gt;watching&lt;/code&gt; treats a short open palm as play or pause. &lt;code&gt;approval_pending&lt;/code&gt; steals that palm and turns it into refuse. I learned the hard way that one vocabulary is not enough if the same hand both pauses a film and kills a command. The hold ring is the whole safety story. 800 ms is the floor. A critical command raises it to 2400 ms. The ring is a progress fraction of that longer number, so a 0.8s thumb on &lt;code&gt;rm -rf&lt;/code&gt; still looks half empty.&lt;/p&gt;

&lt;p&gt;Finding the main video is a scorer, not a site whitelist. YouTube-shaped pages have &lt;code&gt;html5-main-video&lt;/code&gt;. Netflix-shaped pages have a player view class. Tiny autoplay tiles lose. Hidden nodes lose. Cross-origin iframes are honest about what they cannot reach. That work lives in &lt;code&gt;src/lib/find-video.ts&lt;/code&gt; and the content script.&lt;/p&gt;

&lt;p&gt;The localhost bridge is a WebSocket on &lt;code&gt;127.0.0.1:4537&lt;/code&gt;. CLI, extension, and &lt;code&gt;/watch&lt;/code&gt; all speak the same tiny protocol: hello, ask, answer, status. One ask at a time. If you time out, the command is not run. If a second ask arrives while one is pending, the new one is rejected. Refuse by default is a real default.&lt;/p&gt;

&lt;p&gt;Week 3 is crowded with download tidiers and overnight chat summaries. I kept the gesture build because I could not find another Week 3 post that mixed a webcam veto with a local model. I did find plenty of prior art outside the tag, and I used it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tapproval.thoughtfulsteward.org/" rel="noopener noreferrer"&gt;Tapproval&lt;/a&gt; marks a force-push CRITICAL on an Apple Watch, then one tap answers. I stole the mark and spent it on time. The card still says CRITICAL. The thumb has to stay up longer. &lt;a href="https://github.com/8796n/nyandsign" rel="noopener noreferrer"&gt;NyandSign&lt;/a&gt; taught me the wake-sign idea (camera ignores you until you arm it). I did not ship that tonight. The risk line plus the longer hold was the sharper story against GestureApprove. Banking push-approve and Steam Big Picture already taught people that a destructive action should feel heavier than &lt;code&gt;echo&lt;/code&gt;. Goblin Tools Magic ToDo gave me the "one line, then decide" shape. Lazy.so gave me the "capture the thing in front of you without leaving" shape. I pointed both at a pending shell command.&lt;/p&gt;

&lt;p&gt;Gemma 2 2B IT q4 through WebLLM is the engine I want for both the film recap and the command one-liner. It needs WebGPU. This VM has no &lt;code&gt;navigator.gpu&lt;/code&gt;, even with SwiftShader flags. DistilBART-CNN-6-6 through transformers.js is what actually wrote the Big Buck Bunny recap: 4867 ms to load, 572 ms to infer, on CPU. The command path uses those same loaders. If both miss, the recap falls back to an extractive caption slice and the risk line stays on the heuristic. The card says which engine spoke.&lt;/p&gt;

&lt;p&gt;The fixture tests measured what I could measure without a living-room camera. Pose fixtures hit 60/60 for palm, thumb, and none. Swipes hit 32/32. Forty noisy hold sequences produced zero false approvals. DistilBART produced a real recap I kept in &lt;code&gt;docs/MODEL_RUN.md&lt;/code&gt;. New tests check that &lt;code&gt;echo&lt;/code&gt; still clears at 800 ms and that &lt;code&gt;git push --force&lt;/code&gt; does not.&lt;/p&gt;

&lt;p&gt;What failed: Gemma did not start here. Live YouTube captions on a signed-in watch page were not smoked on a Mac. Netflix DRM was not smoked. The Mac with the webcam is offline, so these demo files are Playwright recordings, not a handheld take from my sofa. Those limits are written down in &lt;code&gt;docs/LIMITS.md&lt;/code&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why Open Innovation Matters
&lt;/h1&gt;

&lt;p&gt;A webcam pointed at a sofa is a face. Captions from a film I have not finished are plot. A pending shell line is sometimes a secret path. I do not want those three things in one cloud request so a recap can sound smarter.&lt;/p&gt;

&lt;p&gt;Offline also means I can refuse a command when the cafe wifi dies. There is no inference bill for holding a thumb. DistilBART and the extractive path both keep the product honest on a machine that cannot load Gemma.&lt;/p&gt;

&lt;p&gt;I can swap the open model without rewriting the player. &lt;code&gt;src/lib/summarize.ts&lt;/code&gt; already has three engines behind one function. The risk module keeps the tier on the heuristic so a warmer sentence cannot shorten the hold.&lt;/p&gt;

&lt;p&gt;A closed API would have been the worse fit because the whole point of the veto is that the picture of my hand, the film I am mid-way through, and the command that might delete &lt;code&gt;/tmp&lt;/code&gt; stay on the laptop I already paid for.&lt;/p&gt;

&lt;h1&gt;
  
  
  Prize Categories
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Best Use of Gemma&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gemma 2 2B IT is the preferred on-device engine for the miss recap and for the one-line command blurb, loaded through WebLLM when WebGPU exists. DistilBART is the measured fallback on this VM. I am not listing other partner categories I did not actually use.&lt;/p&gt;

&lt;h1&gt;
  
  
  Credits
&lt;/h1&gt;

&lt;p&gt;MediaPipe Hands and BlazeFace (Apache 2.0) read the landmarks. The bundled clips are official Big Buck Bunny and Sintel trailers from the Blender Foundation, CC BY 3.0, re-encoded to 480p. Gemma terms apply if you download those weights. DistilBART-CNN-6-6 is Apache 2.0.&lt;/p&gt;

&lt;p&gt;Prior art I am crediting on purpose: GestureApprove, NyandSign, Gestro, Nod Code, Tapproval, Moshi, Perch, GrantTap, and Couchmode. Steal notes with links: &lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/STEAL_LOG.md" rel="noopener noreferrer"&gt;docs/STEAL_LOG.md&lt;/a&gt;. Honest edges: &lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/LIMITS.md" rel="noopener noreferrer"&gt;docs/LIMITS.md&lt;/a&gt;. Fixture numbers: &lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/MEASUREMENTS.md" rel="noopener noreferrer"&gt;docs/MEASUREMENTS.md&lt;/a&gt;. The DistilBART recap that actually ran: &lt;a href="https://github.com/Clintobi/lazy-hands/blob/main/docs/MODEL_RUN.md" rel="noopener noreferrer"&gt;docs/MODEL_RUN.md&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  devchallenge #hf26challenge
&lt;/h1&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
