@eigenwert/pi-gatekeeper
Pi extension that gates file-mutating tool calls behind user approval with AST-based bash command analysis
Package details
Install @eigenwert/pi-gatekeeper from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@eigenwert/pi-gatekeeper- Package
@eigenwert/pi-gatekeeper- Version
2.1.0- Published
- Apr 1, 2026
- Downloads
- 556/mo ยท 30/wk
- Author
- eigenwert
- License
- MIT
- Types
- extension
- Size
- 58.2 KB
- Dependencies
- 2 dependencies ยท 0 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
๐ก๏ธ Gatekeeper
A pi extension that adds a permission system. File-mutating tool calls require user approval before execution.
How It Works
Gatekeeper uses a default-deny model with AST-based analysis:
editandwritetool calls are always gatedbashcommands are parsed into an AST using tree-sitter-bash, then classified:- Every command in the AST is checked against a safe command allowlist
- Output redirections (
>,>>) are detected structurally - Dynamic/unresolvable constructs (command substitution, variable expansion, ANSI-C strings in command position) are always gated
- Benign command wrappers (
env,nice,nohup,timeout, etc.) are unwrapped to check the inner command - Compound commands (
&&,||,;, pipes, loops, conditionals) have every sub-command checked - Parse errors trigger gating (possible obfuscation)
- Falls back to regex patterns if tree-sitter is unavailable
Design principle: if the analyzer can't prove a command is safe, it's gated. False positives (unnecessary prompts) are annoying but harmless. False negatives (missed mutations) are dangerous.
Allowed without asking
Commands on the allowlist pass through silently. This includes:
| Category | Commands |
|---|---|
| File viewing | cat, head, tail, less, bat |
| Listing | ls, tree, exa, eza |
| Search | grep, rg, ag, find (without -delete/-exec), fd |
| File info | file, stat, wc, du, df, readlink |
| Text processing | awk, sed (without -i), sort (without -o), uniq, cut, tr, jq |
| Comparison | diff, cmp, comm |
| System info | whoami, uname, env, printenv, uptime |
| Shell utils | echo, printf, pwd, which, type, test, cd |
| Network (read) | ping, dig, curl (without -o/-O), wget --spider |
| Git (read) | status, log, diff, show, blame, branch (list), tag (list) |
| Pkg mgrs (read) | npm ls, npm outdated, npm audit, cargo check, pip list |
| Interpreters | Only --version / -v checks |
| Redirects | > /dev/null, 2>&1, < input (input redirects) |
See allowlist.ts for the complete list.
Gated (requires approval)
Everything not on the allowlist, including:
- File mutation:
rm,mv,cp,mkdir,touch,chmod,ln,rsync,tee,dd, ... - Output redirects:
> file.txt,>> file.txt - Git mutations:
push,commit,add,checkout,reset,rebase,merge, ... - Package mutations:
npm install,npm run,yarn add, ... - Shell execution:
bash -c,sh -c,eval,source,exec - Interpreters with code:
python3 -c,node -e,ruby -e, ... - Privilege escalation:
sudo,doas - Obfuscation:
$(cmd)in command position,$varin command position, ANSI-C strings, parse errors
Obfuscation resistance
The AST-based approach defeats common evasion techniques:
| Technique | How it's caught |
|---|---|
$(echo rm) file |
Dynamic command name (command substitution) |
$cmd file |
Dynamic command name (variable expansion) |
$'\x72\x6d' file |
Dynamic command name (ANSI-C string) |
/bin/rm file |
Path stripped โ rm โ not in allowlist |
\rm file |
Backslash stripped โ rm โ not in allowlist |
"rm" file / 'rm' file |
Quoted name resolved โ rm โ not in allowlist |
env rm file |
Wrapper unwrapped โ rm โ not in allowlist |
nice env nohup rm file |
Stacked wrappers unwrapped โ rm |
echo x | base64 -d | sh |
sh not in allowlist |
echo x > file.txt |
Output redirect detected structurally |
echo safe; rm bad |
Every sub-command checked |
for f in *; do rm $f; done |
Loop body checked |
| Malformed input | Parse errors โ gated |
Dialog
When a gated tool call is intercepted, a dialog appears showing what the tool wants to do (including full diffs for edits).
| Key | Action |
|---|---|
y |
Accept this tool call |
n |
Decline this tool call |
a |
Switch to auto-accept mode |
โ โ |
Navigate options |
Tab |
Attach a message to Yes/No (the LLM sees your feedback) |
Enter |
Confirm highlighted option |
Esc |
Decline |
Settings
Open with /gatekeeper:
| Setting | Values | Description |
|---|---|---|
| Mode | ask (default), auto-accept |
Whether tool calls require approval |
| Show detection reasons | off (default), on |
Show why a bash command was gated (e.g. "output redirect", "unknown command") |
Install
pi install npm:@eigenwert/pi-gatekeeper
# or
pi install git:github.com/noel-debug/pi-gatekeeper
Or manually:
mkdir -p ~/.pi/agent/extensions/gatekeeper
cp index.ts dialog.ts patterns.ts analyzer.ts allowlist.ts package.json ~/.pi/agent/extensions/gatekeeper/
cd ~/.pi/agent/extensions/gatekeeper && npm install
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Incoming bash command โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ tree-sitter-bash AST parse โ
โ Parse errors โ GATE โ
โ Falls back to regex if WASM unavailable โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AST walk โ classify every node โ
โ โข command โ resolve name, unwrap wrappers โ
โ โข file_redirect โ check for >, >> โ
โ โข dynamic constructs โ GATE โ
โ โข function_definition โ GATE โ
โ โข recurse into compound/control-flow nodes โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Default-deny allowlist check โ
โ Command in allowlist with safe args โ ALLOW โ
โ Unknown command โ GATE โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
ALLOW or GATE
Try to break it
No static analysis of a Turing-complete language is perfect. Try to sneak a touch /tmp/pwned past the filter. Try to find a read-only command that gets blocked for no reason. The test suite has n cases โ see if you can find case n+1.
License
MIT