npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pi-blocklist

v0.1.1

Published

Blocks destructive shell commands before a pi agent runs them. A linter for accidents, not a sandbox.

Downloads

81

Readme

pi-blocklist

A pi extension that stops an agent from running destructive shell commands.

It parses each command the agent is about to execute, works out which program will run and which paths that program will actually touch after expansion, and refuses the ones that would destroy your machine or your data.

The guard refusing a deletion whose target depends on an unset variable

> rm -rf "$STEAMROOT/"*

Blocked by command-blocklist. `rm $STEAMROOT/*` targets /* if that variable is unset
or empty. Assign it and guard with `${VAR:?}` before deleting.

Segment: rm -rf "$STEAMROOT/"*
Rule: rm.unset-variable

Read this before you install it

This is a linter, not a sandbox. It reads a command as text. Anything computed at runtime is invisible to it:

bash -c "$(echo cm0gLXJmIC8K | base64 -d)"   # runs `rm -rf /` — not caught

So is a script written by the write tool and executed on the next turn, and so is any compiled binary. Deciding those requires running them.

What it does catch is accidents — the flag you fat-fingered, the variable that was never set, the cd you forgot you were still inside. That is what actually destroys people's data. Treat it as a seatbelt, not a locked door. If you need a real boundary, run the agent in a container.


What it can do to your machine

Worth checking rather than trusting, for something that sees every command your agent runs:

  • command-blocklist/analyze.ts has no imports at all — pure string processing, no I/O.
  • command-blocklist/index.ts imports only appendFileSync, homedir, platform, join.
  • Zero runtime dependencies.

The entire trust surface is appending to one log file inside your pi agent directory. No network, no subprocesses, no eval.

Package details

| | | |---|---| | Type | extension | | Entry point | command-blocklist/index.ts | | Dependencies | none (the analyzer is pure TypeScript) | | Requires | pi >= 0.74.0 (every release published to npm exports the APIs it uses) | | Verified on | pi 0.85.1, macOS 26 (arm64), Ubuntu 26.04 (x86_64) | | Platforms | POSIX shells. See Platform support |


Install

pi install npm:pi-blocklist

Project-locally instead of globally:

pi install npm:pi-blocklist -l

Straight from the repository, to track main:

pi install git:github.com/SeraphimSerapis/pi-blocklist

From a working copy, without installing:

pi -e ./command-blocklist/index.ts

Or drop it straight into your extensions directory — ~/.pi/agent/extensions/ for all sessions, ./.pi/extensions/ for one project:

ln -s ~/Code/pi-blocklist/command-blocklist ~/.pi/agent/extensions/command-blocklist

Verify it loaded:

/blocklist-check rm -rf /

What you get

Three surfaces are guarded:

  • The bash tool — the agent's main way to run commands.
  • Custom and MCP tools that shell out. These arrive as ordinary tool calls and would otherwise skip the guard entirely. Any tool carrying a string command, cmd, or shell_command field is checked; add exact tools to EXPLICIT_SHELL_TOOLS in index.ts.
  • Your own !command typing — deny-tier only, since you typed it deliberately.

Plus a /blocklist-check <command> slash command that dry-runs a command through the rules without executing it, and an append-only JSONL audit log at $PI_CODING_AGENT_DIR/command-blocklist.log (default ~/.pi/agent/command-blocklist.log) recording every decision:

{"at":"2026-09-07T17:57:38.195Z","verdict":"deny","tool":"bash","rule":"rm.recursive-critical","command":"rm -rf /"}

When the guard misfires, that log is the evidence. Read it before you change a rule. It is created mode 0600, because commands routinely carry secrets as arguments.


Two tiers

| Verdict | Behaviour | |---|---| | deny | Refused. The agent is told why and can adjust. | | confirm | Prompts you; runs only on an explicit yes. |

The confirm tier prompting before a git reset --hard

The second tier exists to protect the first. Without somewhere to put git reset --hard and apt purge, everything gets crammed into deny, the guard starts blocking real work, and you turn it off. That is the failure mode this design cares most about.

With no UI attached — pi -p, RPC — a confirm fails closed and blocks, telling the agent that confirmation was needed and nobody was there to give it. An aborted prompt is also not consent.


How it works

command string
  │
  ├─ 1. lift out heredoc bodies          data, not commands
  ├─ 2. tokenize                          quotes, escapes, $'…', ${…}, $(…), backticks
  │                                       split on ; && || | & and newlines
  ├─ 3. resolve each segment              strip VAR=x, sudo, env, xargs, chroot /mnt
  │                                       → basename(argv[0])
  ├─ 4. classify paths                    expand ~ $HOME $PWD .. and in-command vars
  │                                       → critical | sensitive | safe
  └─ 5. run 19 rules                      most severe finding wins

The whole design rests on asking two structural questions — what program runs, and what path does it actually touch — rather than matching the command text against patterns. Pattern matching is simultaneously too loose and too tight: a glob for rm -rf /* also matches rm -rf /tmp/build, and one for halt* blocks cat asphalt.txt.

Because the questions are structural, one rm rule covers every spelling at once:

rm -rf /            rm -fr /              rm -r -f /
rm --recursive --force /                  rm -rf "$HOME"
rm -rf ~            rm -rf /users/tim     # macOS filesystems are case-insensitive
FOO="a b" rm -rf /  sudo -u root rm -rf /  xargs rm -rf /
cd / && rm -rf .    rm -rf \<newline>/    bash <<< 'rm -rf /'
find / -name '*.log' | xargs rm -rf       echo 'rm -rf /' | bash

Path tiers

| Tier | Examples | |---|---| | critical | /, $HOME, system roots at depth ≤ 2 — /etc, /usr/local, /Users/you, /home/you | | sensitive | deeper under a system root, or one level under home — ~/Documents, /usr/local/lib/x | | safe | everything else, plus scratch (/tmp, /var/folders, /dev/shm) and /dev/null |

A glob is judged by its shallowest component, so /home/*/Documents is treated as /home.


What it catches

| Area | Examples | |---|---| | Deletion | rm at root/home/system paths, --no-preserve-root, find -delete, find \| xargs rm, shred | | Unset variables | rm -rf "$UNSET/"* — judged by its empty expansion | | Disks | dd of=/dev/…, > /dev/sda, tee /dev/sda, mkfs, wipefs, fdisk, diskutil erase* | | Storage stack | lvremove, vgremove, blkdiscard, mdadm --zero-superblock, cryptsetup luksErase | | Power | shutdown, reboot, halt, poweroff, systemctl poweroff, init 0 | | System files | truncating /etc/passwd, chmod -R on /, cp/ln/tee over critical paths | | Posture | csrutil disable, spctl --master-disable, nvram -c | | Accounts | userdel -r, deluser --remove-home | | Confirm tier | git reset --hard, git clean -fdx, git push --force, apt purge, dpkg --purge, grub-install, iptables -F, curl \| sh |

And what it deliberately leaves alone

False positives are how a guard gets disabled, so these stay silent:

cat asphalt.txt                       # `halt*` used to match this
grep -r shutdown src/                 # the word is not the command
./scripts/reboot-staging.sh
rm -rf node_modules && rm -rf ./dist
rm -rf /tmp/build-cache
OUT=dist; rm -rf "$OUT"/*             # assigned in-command, so resolvable
rm -rf "${BUILD_DIR:?}"/*             # the guard idiom the deny message recommends
sudo apt-get install -y jq
find . -name '*.tmp' -delete
cat > doc.md <<'EOF'                  # a heredoc body is data
never run rm -rf /
EOF

Related distinctions the rules make rather than flattening: > /etc/passwd denies but >> /etc/hosts only confirms, because truncating a system file and appending a line to one are different acts. Same for rm -rf ~/Code/project/dist (allowed, inside the working directory) against rm -rf ~/Downloads (confirm).


Tuning it

Rules live in command-blocklist/analyze.ts as small functions of one shape. To add a whole-command pattern without writing code, append to LEGACY_DENY_GLOBS:

export const LEGACY_DENY_GLOBS = [
  "diskutil erase*",
  "mkfs*",
  "shutdown*",
];

These are anchored and matched per segment, so halt* means "this command starts with halt". They are a poor way to express a path policy — * crosses /, so rm -rf /* would also match rm -rf /tmp/build. For anything path-shaped, add a rule instead and let classifyPath do the work.

Whether a given command should deny or confirm is a judgement call baked into the source. If you disagree with one, change the verdict on that rule and add a test both ways.


Platform support

| Platform | Status | |---|---| | macOS | Verified. Case-insensitive path matching, diskutil, csrutil, spctl, tmutil, pmset | | Debian / Ubuntu | Verified on Ubuntu 26.04. systemd, LVM/RAID/LUKS, apt/dpkg, grub, iptables/nft/ufw | | Other Linux | Should work; the POSIX path model is shared. Untested | | Windows | POSIX shells only. The rules apply under WSL and Git Bash, but know nothing about cmd or PowerShell — format C:, del /s, rmdir /s all pass. The extension warns at session start on win32 |


Development

node --test command-blocklist/analyze.test.ts

206 cases covering both a macOS and a Debian/Ubuntu path layout, the tokenizer, the path classifier, runtime bounds on pathological input, and a seeded fuzz pass over 20,000 generated shell fragments (asserting the analyzer neither throws nor stalls on hostile input).

Running the suite needs a Node build with TypeScript support — 23.6+ has it on by default, 22.6+ behind --experimental-strip-types. Some distro builds ship without it and fail with ERR_NO_TYPESCRIPT; that affects only the tests, since pi loads extensions through jiti, which transpiles on its own.

analyze.ts has no dependencies and no I/O, and imports nothing from pi — it is reusable anywhere you need to judge a shell command. index.ts is the pi adapter and the only file that knows pi exists.

Releasing

package.json declares the entry point through the pi.extensions manifest and carries the pi-package keyword that lists it on pi.dev/packages. Publishing is npm publish; tag the matching commit.


License

MIT — see LICENSE. Copyright (c) 2026 Tim Messerschmidt.