Wraps 18 package managers — npm, pnpm, yarn, bun, npx, pip, pip3, uv, poetry, pipenv, pdm, pipx, cargo, gem, bundle, go, composer, dotnet — as PATH shims, plus a Claude Code pre-tool-use hook. Refuses to install packages with known CVEs.
$ npm install lodash@4.17.10
refuse: blocked — CVE-2019-10744 (high)
Prototype pollution in lodash <= 4.17.11
suggested safe version: 4.17.21refuse sits in front of your package managers. Every install call is vetted against a refuse server (self-hosted) or refuse.dev (hosted) before the real binary runs. If the package has a known advisory above your severity threshold, the install is blocked with the CVE and a suggested safe version.
Works:
- On a developer's laptop.
- In a CI runner.
- Inside a Docker build stage.
- As a Claude Code PreToolUse hook — same gate for autonomous agent installs.
Homebrew (macOS):
brew install refusehq/tap/refuseDirect binary on macOS / Linux:
curl -sSL https://raw.githubusercontent.com/RefuseHQ/refuse-cli/main/scripts/install.sh | shDirect binary on Windows (PowerShell):
irm https://raw.githubusercontent.com/RefuseHQ/refuse-cli/main/scripts/install.ps1 | iexFrom source:
go install github.com/RefuseHQ/refuse-cli/cmd/refuse@latestPlatforms. Pre-built binaries are published for:
| OS | Architectures |
|---|---|
| macOS | x86_64, arm64 |
| Linux | x86_64, arm64, i386, armv6, armv7 |
| Windows | x86_64, arm64, i386 |
Other platforms can go install from source.
refuse init # interactive: server URL + API key
refuse install # writes shims to ~/.refuse/bin + updates PATH
refuse hook install claude-code # PreToolUse hook in ~/.claude/settings.json
refuse python-hook install # closes the `python -m pip` bypass (per Python env)Then run anything you'd normally run:
npm install express
pip install requests
cargo add tokioIf the install is clean, it goes through. If it isn't, refuse blocks it and tells you why.
The CLI is the gate; the server is the brain. Two ways to bring up a backend — pick either:
Hosted (refuse.dev)
refuse config set server_url https://mcp.refuse.dev
refuse config set api_key rfs_...Self-hosted (RefuseHQ/refuse — Apache-2.0, single Docker container, no signup)
docker run -d --name refuse -p 8080:8080 \
-v refuse-data:/data \
ghcr.io/refusehq/refuse:latest
refuse config set server_url http://localhost:8080The server ingests OSV, CISA KEV, FIRST EPSS, GHSA, deps.dev, and Wolfi; first-boot bootstrap takes ~3 min, then deltas every 5 min. GET /readyz flips from 503 to 200 when the seed completes. Both editions speak the same /api/v1/check/* API, so switching between hosted and self-host is a one-line config change.
| Manager | Ecosystem | Install verbs | Lockfile parsing |
|---|---|---|---|
npm |
npm | install, i, add |
package-lock.json |
pnpm |
npm | install, add |
pnpm-lock.yaml |
yarn (classic + Berry) |
npm | add, install |
yarn.lock |
bun |
npm | install, add |
bun.lockb / bun.lock |
pip / pip3 |
PyPI | install, install -r |
requirements.txt |
cargo |
crates.io | add, install |
Cargo.lock |
gem |
RubyGems | install |
Gemfile.lock |
go |
Go modules | get, install |
go.sum |
| Agent | Status |
|---|---|
| Claude Code | ✓ supported |
| Cursor | tracked |
| Continue | tracked |
| Aider | tracked |
| Codex CLI | tracked |
| Cline | tracked |
PRs welcome — see internal/hook/claudecode.go as the reference.
| Command | What it does |
|---|---|
refuse init |
First-time setup wizard |
refuse install |
Install shims for the supported package managers |
refuse uninstall |
Remove shims + revert shell-rc edits |
refuse hook install <agent> |
Write a pre-tool-use hook for <agent> |
refuse hook remove <agent> |
Remove that hook |
refuse hook list |
Show all installed hooks |
refuse check <eco> <pkg>[@<ver>] |
One-off check |
refuse check-lockfile <path> |
Scan an entire lockfile |
refuse audit [path] |
One-shot scan of every lockfile / Dockerfile / GH-Actions workflow under a directory |
refuse gate |
The decision engine — shims + hooks call this on stdin |
refuse pip-gate |
Variant of refuse gate for pip's argument grammar; invoked by the pip shim. |
refuse config show | set | get |
Manage ~/.refuse/config.yaml |
refuse status |
Diagnose install state |
refuse doctor |
Verify PATH / hooks / server reachability |
refuse audit # walk the repo, scan lockfiles + Dockerfiles + workflows
refuse audit --json > out.jsonrefuse config set <key> <value>, or edit ~/.refuse/config.yaml directly:
server_url: http://localhost:8080 # or https://mcp.refuse.dev
api_key: rfs_... # optional, required for hosted
policy:
severity_threshold: high # low | medium | high | critical
fail_closed: false # true = block if server unreachable (default false → fail open with stderr warning)
allow_yanked: false # allow yanked versions when no advisory matches
allow_prerelease: false # allow prerelease versions
override_env: REFUSE_ALLOW_VULNERABLE # env var that forces a block to pass throughEnvironment variables override the file (useful in CI):
REFUSE_SERVER_URLREFUSE_API_KEYREFUSE_POLICY— setsseverity_thresholdREFUSE_FAIL_CLOSED—1/trueto enableREFUSE_ALLOW_VULNERABLE—1/trueto bypass a single installREFUSE_TIMEOUT_MS— HTTP timeout in milliseconds (default8000)REFUSE_NO_GATE—1to skip the gate entirely for the next call (debug)
Append --no-refuse to any wrapped command to skip the gate just this once.
refuse strips the flag before running the real manager:
npm install lodash@4.17.10 --no-refuse # runs the real npm, ungated
pip install pyyaml==5.3 --no-refuseEquivalent to a one-shot REFUSE_NO_GATE=1, but inline. This only works
through the PATH shim (interactive use) — the agent PreToolUse hook
ignores it, so an autonomous agent can't bypass its own gate.
The PATH shim is a convenient first line, but anything that bypasses it
(python -m pip, uv, absolute-path calls, conda install, a Dockerfile
layer that doesn't inherit the shim PATH) won't be gated. The deterministic
gate is refuse check-lockfile — it inspects the resolved manifest no
matter how a dep got there.
Wire it into both ends of the pipeline:
refuse proxy(preview) — local HTTPS forward proxy that gates registry traffic for ecosystems the shim can't reach (Buildkit, isolated CI agents). Status: experimental. See docs/design/registry-proxy.md.
Block a vulnerable lockfile from being committed in the first place. In
your project's .pre-commit-config.yaml:
repos:
- repo: https://github.com/RefuseHQ/refuse-cli
rev: <version> # pin a refuse release — see https://github.com/RefuseHQ/refuse-cli/releases
hooks:
- id: refuse-check
# Optional, heavier — only on push:
- id: refuse-check-installed-piprefuse-check scans every common lockfile that changed in the commit
(package-lock.json, yarn.lock, pnpm-lock.yaml, requirements.txt,
Pipfile.lock, poetry.lock, pdm.lock, uv.lock, Cargo.lock,
Gemfile.lock, go.sum, composer.lock, pom.xml, *.csproj,
mix.lock, pubspec.lock) and aborts the commit on any vulnerable hit.
refuse-check-installed-pip runs on pre-push only and pipes
pip freeze through refuse check-lockfile, catching deps that arrived
via python -m pip, uv pip, or conda install.
Use the bundled composite action:
# .github/workflows/ci.yaml
jobs:
refuse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: RefuseHQ/refuse-cli@v1
with:
api-key: ${{ secrets.REFUSE_API_KEY }}
# lockfiles: | # optional override — auto-discovers if blank
# package-lock.json
# requirements.txtAuto-discovers every supported lockfile in the workspace and fails the
build on a vulnerable hit. See action.yml.
| Input | Default | Required | Description |
|---|---|---|---|
server-url |
https://mcp.refuse.dev |
no | refuse server URL (override for self-host). |
api-key |
none | no | refuse API key (rfs_...). Required for hosted; optional for localhost self-host. |
lockfiles |
auto-discover | no | Newline-separated list of lockfile paths. Auto-discovers common formats if empty. |
version |
latest |
no | refuse-cli version to install. |
severity |
none | no | Severity floor (low | medium | high | critical). |
fail-on-error |
true |
no | Fail the workflow on server/network errors. false falls through. |
# Scan an explicit lockfile
refuse check-lockfile package-lock.json
# Scan currently-installed pip set (catches `python -m pip` / conda)
pip freeze | refuse check-lockfile --filename=requirements.txt /dev/stdinPRs welcome — particularly for new package managers, agent hooks, and platforms. See CONTRIBUTING.md.
Security policy: SECURITY.md. Report privately via hello@refuse.dev.
Apache License 2.0 © RefuseHQ.