npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cc-safety-net

v0.6.0

Published

Claude Code / OpenCode plugin - block destructive git and filesystem commands before execution

Downloads

4,053

Readme

Claude Code Safety Net

CI codecov Version Claude Code OpenCode Gemini CLI License: MIT

CC Safety Net

A Claude Code plugin that acts as a safety net, catching destructive git and filesystem commands before they execute.

Contents

Why This Exists

We learned the hard way that instructions aren't enough to keep AI agents in check. After Claude Code silently wiped out hours of progress with a single rm -rf ~/ or git checkout --, it became evident that soft rules in an CLAUDE.md or AGENTS.md file cannot replace hard technical constraints. The current approach is to use a dedicated hook to programmatically prevent agents from running destructive commands.

Why Use This Instead of Permission Deny Rules?

Claude Code's .claude/settings.json supports deny rules with wildcard matching (e.g., Bash(git reset --hard:*)). Here's how this plugin differs:

At a Glance

| | Permission Deny Rules | Safety Net | |---|---|---| | Setup | Manual configuration required | Works out of the box | | Parsing | Wildcard pattern matching | Semantic command analysis | | Execution order | Runs second | Runs first (PreToolUse hook) | | Shell wrappers | Not handled automatically (must match wrapper forms) | Recursively analyzed (5 levels) | | Interpreter one-liners | Not handled automatically (must match interpreter forms) | Detected and blocked |

Permission Rules Have Known Bypass Vectors

Even with wildcard matching, Bash permission patterns are intentionally limited and can be bypassed in many ways:

| Bypass Method | Example | |---------------|---------| | Options before value | curl -X GET http://evil.com bypasses Bash(curl http://evil.com:*) | | Shell variables | URL=http://evil.com && curl $URL bypasses URL pattern | | Flag reordering | rm -r -f / bypasses Bash(rm -rf:*) | | Extra whitespace | rm -rf / (double space) bypasses pattern | | Shell wrappers | sh -c "rm -rf /" bypasses Bash(rm:*) entirely |

Safety Net Handles What Patterns Can't

| Scenario | Permission Rules | Safety Net | |----------|------------------|------------| | git checkout -b feature (safe) | Blocked by Bash(git checkout:*) | Allowed | | git checkout -- file (dangerous) | Blocked by Bash(git checkout:*) | Blocked | | rm -rf /tmp/cache (safe) | Blocked by Bash(rm -rf:*) | Allowed | | rm -r -f / (dangerous) | Allowed (flag order) | Blocked | | bash -c 'git reset --hard' | Allowed (wrapper) | Blocked | | python -c 'os.system("rm -rf /")' | Allowed (interpreter) | Blocked |

Defense in Depth

PreToolUse hooks run before the permission system. This means Safety Net inspects every command first, regardless of your permission configuration. Even if you misconfigure deny rules, Safety Net provides a fallback layer of protection.

Use both together: Permission deny rules for quick, user-configurable blocks; Safety Net for robust, bypass-resistant protection that works out of the box.

What About Sandboxing?

Claude Code offers native sandboxing that provides OS-level filesystem and network isolation. Here's how it compares to Safety Net:

Different Layers of Protection

| | Sandboxing | Safety Net | |---|---|---| | Enforcement | OS-level (Seatbelt/bubblewrap) | Application-level (PreToolUse hook) | | Approach | Containment — restricts filesystem + network access | Command analysis — blocks destructive operations | | Filesystem | Writes restricted (default: cwd); reads are broad by default | Only destructive operations blocked | | Network | Domain-based proxy filtering | None | | Git awareness | None | Explicit rules for destructive git operations | | Bypass resistance | High — OS enforces boundaries | Lower — analyzes command strings only |

Why Sandboxing Isn't Enough

Sandboxing restricts filesystem + network access, but it doesn't understand whether an operation is destructive within those boundaries. These commands are not blocked by the sandbox boundary:

[!NOTE] Whether they're auto-run or require confirmation depends on your sandbox mode (auto-allow vs regular permissions), and network access still depends on your allowed-domain policy. Claude Code can also retry a command outside the sandbox via dangerouslyDisableSandbox (with user permission); this can be disabled with allowUnsandboxedCommands: false.

| Command | Sandboxing | Safety Net | |---------|------------|------------| | git reset --hard | Allowed (within cwd) | Blocked | | git checkout -- . | Allowed (within cwd) | Blocked | | git stash clear | Allowed (within cwd) | Blocked | | git push --force | Allowed (if remote domain is allowed) | Blocked | | rm -rf . | Allowed (within cwd) | Blocked |

Sandboxing sees git reset --hard as a safe operation—it only modifies files within the current directory. But you just lost all uncommitted work.

When to Use Sandboxing Instead

Sandboxing is the better choice when your primary concern is:

  • Prompt injection attacks — Reduces exfiltration risk by restricting outbound domains (depends on your allowed-domain policy)
  • Malicious dependencies — Limits filesystem writes and network access by default (subject to your sandbox configuration)
  • Untrusted code execution — OS-level containment is stronger than pattern matching
  • Network control — Safety Net has no network protection

Recommended: Use Both

They protect against different threats:

  • Sandboxing contains blast radius — even if something goes wrong, damage is limited to cwd and approved network domains
  • Safety Net prevents footguns — catches git-specific mistakes that are technically "safe" from the sandbox's perspective

Running both together provides defense-in-depth. Sandboxing handles unknown threats; Safety Net handles known destructive patterns that sandboxing permits.

Prerequisites

  • Node.js: Version 18 or higher is required to run this plugin

Quick Start

Claude Code Installation

/plugin marketplace add kenryu42/cc-marketplace
/plugin install safety-net@cc-marketplace

[!NOTE] After installing the plugin, you need to restart your Claude Code for it to take effect.

Claude Code Auto-Update

  1. Run /plugin → Select Marketplaces → Choose cc-marketplace → Enable auto-update

OpenCode Installation

Option A: Let an LLM do it

Paste this into any LLM agent (Claude Code, OpenCode, Cursor, etc.):

Install the cc-safety-net plugin in `~/.config/opencode/opencode.json` (or `.jsonc`) according to the schema at: https://opencode.ai/config.json

Option B: Manual setup

  1. Add the plugin to your config ~/.config/opencode/opencode.json (or .jsonc):
{
  "plugin": ["cc-safety-net"]
}

Gemini CLI Installation

gemini extensions install https://github.com/kenryu42/gemini-safety-net

[!IMPORTANT] You need to set the following settings in .gemini/settings.json to enable hooks:

{
  "tools": {
    "enableHooks": true
  }
}

Status Line Integration

Safety Net can display its status in Claude Code's status line, showing whether protection is active and which modes are enabled.

Setup via Slash Command

The easiest way to configure the status line is using the built-in slash command:

/set-statusline

This interactive command will:

  1. Ask whether you prefer bunx or npx
  2. Check for existing status line configuration
  3. Offer to replace or pipe with existing commands
  4. Write the configuration to ~/.claude/settings.json

Manual Setup

Add the following to your ~/.claude/settings.json:

Using Bun (recommended):

{
  "statusLine": {
    "type": "command",
    "command": "bunx cc-safety-net --statusline"
  }
}

Using npm:

{
  "statusLine": {
    "type": "command",
    "command": "npx -y cc-safety-net --statusline"
  }
}

Piping with existing status line:

If you already have a status line command, you can pipe Safety Net at the end:

{
  "statusLine": {
    "type": "command",
    "command": "your-existing-command | bunx cc-safety-net --statusline"
  }
}

Changes take effect immediately — no restart needed.

Emoji Mode Indicators

The status line displays different emojis based on the current configuration:

| Status | Display | Meaning | |--------|---------|---------| | Plugin disabled | 🛡️ Safety Net ❌ | Safety Net plugin is not enabled | | Default mode | 🛡️ Safety Net ✅ | Protection active with default settings | | Strict mode | 🛡️ Safety Net 🔒 | SAFETY_NET_STRICT=1 — fail-closed on unparseable commands | | Paranoid mode | 🛡️ Safety Net 👁️ | SAFETY_NET_PARANOID=1 — all paranoid checks enabled | | Paranoid RM only | 🛡️ Safety Net 🗑️ | SAFETY_NET_PARANOID_RM=1 — blocks rm -rf even within cwd | | Paranoid interpreters only | 🛡️ Safety Net 🐚 | SAFETY_NET_PARANOID_INTERPRETERS=1 — blocks interpreter one-liners | | Strict + Paranoid | 🛡️ Safety Net 🔒👁️ | Both strict and paranoid modes enabled |

Multiple mode emojis are combined when multiple environment variables are set.

Commands Blocked

| Command Pattern | Why It's Dangerous | |-----------------|-------------------| | git checkout -- files | Discards uncommitted changes permanently | | git checkout <ref> -- <path> | Overwrites working tree with ref version | | git restore files | Discards uncommitted changes | | git restore --worktree | Explicitly discards working tree changes | | git reset --hard | Destroys all uncommitted changes | | git reset --merge | Can lose uncommitted changes | | git clean -f | Removes untracked files permanently | | git push --force / -f | Destroys remote history | | git branch -D | Force-deletes branch without merge check | | git stash drop | Permanently deletes stashed changes | | git stash clear | Deletes ALL stashed changes | | git worktree remove --force | Force-deletes worktree without checking for changes | | rm -rf (paths outside cwd) | Recursive file deletion outside the current directory | | rm -rf / or ~ or $HOME | Root/home deletion is extremely dangerous | | find ... -delete | Permanently removes files matching criteria | | xargs rm -rf | Dynamic input makes targets unpredictable | | xargs <shell> -c | Can execute arbitrary commands | | parallel rm -rf | Dynamic input makes targets unpredictable | | parallel <shell> -c | Can execute arbitrary commands |

Commands Allowed

| Command Pattern | Why It's Safe | |-----------------|--------------| | git checkout -b branch | Creates new branch | | git checkout --orphan | Creates orphan branch | | git restore --staged | Only unstages, doesn't discard | | git restore --help/--version | Help/version output | | git branch -d | Safe delete with merge check | | git clean -n / --dry-run | Preview only | | git push --force-with-lease | Safe force push | | rm -rf /tmp/... | Temp directories are ephemeral | | rm -rf /var/tmp/... | System temp directory | | rm -rf $TMPDIR/... | User's temp directory | | rm -rf ./... (within cwd) | Limited to current working directory |

What Happens When Blocked

When a destructive command is detected, the plugin blocks the tool execution and provides a reason.

Example output:

BLOCKED by Safety Net

Reason: git checkout -- discards uncommitted changes permanently. Use 'git stash' first.

Command: git checkout -- src/main.py

If this operation is truly needed, ask the user for explicit permission and have them run the command manually.

Testing the Hook

You can manually test the hook by attempting to run blocked commands in Claude Code:

# This should be blocked
git checkout -- README.md

# This should be allowed
git checkout -b test-branch

Development

See CONTRIBUTING.md for details on how to contribute to this project.

Custom Rules (Experimental)

Beyond the built-in protections, you can define your own blocking rules to enforce team conventions or project-specific safety policies.

[!TIP] Use /set-custom-rules to create custom rules interactively with natural language.

Quick Example

Create .safety-net.json in your project root:

{
  "version": 1,
  "rules": [
    {
      "name": "block-git-add-all",
      "command": "git",
      "subcommand": "add",
      "block_args": ["-A", "--all", "."],
      "reason": "Use 'git add <specific-files>' instead of blanket add."
    }
  ]
}

Now git add -A, git add --all, and git add . will be blocked with your custom message.

Config File Location

Config files are loaded from two scopes and merged:

  1. User scope: ~/.cc-safety-net/config.json (always loaded if exists)
  2. Project scope: .safety-net.json in the current working directory (loaded if exists)

Merging behavior:

  • Rules from both scopes are combined
  • If the same rule name exists in both scopes, project scope wins
  • Rule name comparison is case-insensitive (MyRule and myrule are considered duplicates)

This allows you to define personal defaults in user scope while letting projects override specific rules.

If no config file is found in either location, only built-in rules apply.

Config Schema

| Field | Type | Required | Description | |-------|------|----------|-------------| | version | integer | Yes | Schema version (must be 1) | | rules | array | No | List of custom blocking rules (defaults to empty) |

Rule Schema

| Field | Type | Required | Description | |-------|------|----------|-------------| | name | string | Yes | Unique identifier (letters, numbers, hyphens, underscores; max 64 chars) | | command | string | Yes | Base command to match (e.g., git, npm, docker) | | subcommand | string | No | Subcommand to match (e.g., add, install). If omitted, matches any. | | block_args | array | Yes | Arguments that trigger the block (at least one required) | | reason | string | Yes | Message shown when blocked (max 256 chars) |

Matching Behavior

  • Commands are normalized to basename (/usr/bin/gitgit)
  • Subcommand is the first non-option argument after the command
  • Arguments are matched literally (no regex, no glob), with short option expansion
  • A command is blocked if any argument in block_args is present
  • Short options are expanded: -Ap matches -A (bundled flags are unbundled)
  • Long options use exact match: --all-files does NOT match --all
  • Custom rules only add restrictions—they cannot bypass built-in protections

Known Limitations

  • Short option expansion: -Cfoo is treated as -C -f -o -o, not -C foo. Blocking -f may false-positive on attached option values.

Examples

Block global npm installs

{
  "version": 1,
  "rules": [
    {
      "name": "block-npm-global",
      "command": "npm",
      "subcommand": "install",
      "block_args": ["-g", "--global"],
      "reason": "Global npm installs can cause version conflicts. Use npx or local install."
    }
  ]
}

Block dangerous docker commands

{
  "version": 1,
  "rules": [
    {
      "name": "block-docker-system-prune",
      "command": "docker",
      "subcommand": "system",
      "block_args": ["prune"],
      "reason": "docker system prune removes all unused data. Use targeted cleanup instead."
    }
  ]
}

Multiple rules

{
  "version": 1,
  "rules": [
    {
      "name": "block-git-add-all",
      "command": "git",
      "subcommand": "add",
      "block_args": ["-A", "--all", ".", "-u", "--update"],
      "reason": "Use 'git add <specific-files>' instead of blanket add."
    },
    {
      "name": "block-npm-global",
      "command": "npm",
      "subcommand": "install",
      "block_args": ["-g", "--global"],
      "reason": "Use npx or local install instead of global."
    }
  ]
}

Error Handling

Custom rules use silent fallback error handling. If your config file is invalid, the safety net silently falls back to built-in rules only:

| Scenario | Behavior | |----------|----------| | Config file not found | Silent — use built-in rules only | | Empty config file | Silent — use built-in rules only | | Invalid JSON syntax | Silent — use built-in rules only | | Missing required field | Silent — use built-in rules only | | Invalid field format | Silent — use built-in rules only | | Duplicate rule name | Silent — use built-in rules only |

[!IMPORTANT]
If you add or modify custom rules manually, always validate them with npx -y cc-safety-net --verify-config or /verify-custom-rules slash command in your coding agent.

Block Output Format

When a custom rule blocks a command, the output includes the rule name:

BLOCKED by Safety Net

Reason: [block-git-add-all] Use 'git add <specific-files>' instead of blanket add.

Command: git add -A

Advanced Features

Strict Mode

By default, unparseable commands are allowed through. Enable strict mode to fail-closed when the hook input or shell command cannot be safely analyzed (e.g., invalid JSON, unterminated quotes, malformed bash -c wrappers):

export SAFETY_NET_STRICT=1

Paranoid Mode

Paranoid mode enables stricter safety checks that may be disruptive to normal workflows. You can enable it globally or via focused toggles:

# Enable all paranoid checks
export SAFETY_NET_PARANOID=1

# Or enable specific paranoid checks
export SAFETY_NET_PARANOID_RM=1
export SAFETY_NET_PARANOID_INTERPRETERS=1

Paranoid behavior:

  • rm: blocks non-temp rm -rf even within the current working directory.
  • interpreters: blocks interpreter one-liners like python -c, node -e, ruby -e, and perl -e (these can hide destructive commands).

Shell Wrapper Detection

The guard recursively analyzes commands wrapped in shells:

bash -c 'git reset --hard'    # Blocked
sh -lc 'rm -rf /'             # Blocked

Interpreter One-Liner Detection

Detects destructive commands hidden in Python/Node/Ruby/Perl one-liners:

python -c 'import os; os.system("rm -rf /")'  # Blocked

Secret Redaction

Block messages automatically redact sensitive data (tokens, passwords, API keys) to prevent leaking secrets in logs.

Audit Logging

All blocked commands are logged to ~/.cc-safety-net/logs/<session_id>.jsonl for audit purposes:

{"ts": "2025-01-15T10:30:00Z", "command": "git reset --hard", "segment": "git reset --hard", "reason": "...", "cwd": "/path/to/project"}

Sensitive data in log entries is automatically redacted.

License

MIT