{"id":50707873,"url":"https://github.com/kenryu42/cc-safety-net","last_synced_at":"2026-06-19T21:00:54.576Z","repository":{"id":330471312,"uuid":"1122873619","full_name":"kenryu42/cc-safety-net","owner":"kenryu42","description":"A coding agent CLI hook that acts as a safety net, catching destructive git and filesystem commands before they execute. Supports Codex, Claude Code, OpenCode, Gemini CLI, Copilot CLI, Kimi Code and Pi.","archived":false,"fork":false,"pushed_at":"2026-06-19T10:52:17.000Z","size":2910,"stargazers_count":1405,"open_issues_count":3,"forks_count":65,"subscribers_count":9,"default_branch":"main","last_synced_at":"2026-06-19T12:27:32.280Z","etag":null,"topics":["claude","claude-code","claude-code-plugin","codex","destructive-commands","kimi-code","pi-extension","security"],"latest_commit_sha":null,"homepage":"https://ccsafetynet.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kenryu42.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":["kenryu42"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":null,"thanks_dev":null,"custom":null}},"created_at":"2025-12-25T17:52:57.000Z","updated_at":"2026-06-19T10:52:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kenryu42/cc-safety-net","commit_stats":null,"previous_names":["kenryu42/claude-code-safety-net","kenryu42/cc-safety-net"],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/kenryu42/cc-safety-net","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenryu42%2Fcc-safety-net","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenryu42%2Fcc-safety-net/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenryu42%2Fcc-safety-net/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenryu42%2Fcc-safety-net/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kenryu42","download_url":"https://codeload.github.com/kenryu42/cc-safety-net/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenryu42%2Fcc-safety-net/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34547787,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-19T02:00:06.005Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["claude","claude-code","claude-code-plugin","codex","destructive-commands","kimi-code","pi-extension","security"],"created_at":"2026-06-09T13:00:37.291Z","updated_at":"2026-06-19T21:00:54.548Z","avatar_url":"https://github.com/kenryu42.png","language":"TypeScript","funding_links":["https://github.com/sponsors/kenryu42"],"categories":["TypeScript"],"sub_categories":[],"readme":"# CC Safety Net\n\n[![CI](https://github.com/kenryu42/cc-safety-net/actions/workflows/ci.yml/badge.svg)](https://github.com/kenryu42/cc-safety-net/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/github/kenryu42/cc-safety-net/branch/main/graph/badge.svg?token=C9QTION6ZF)](https://codecov.io/github/kenryu42/cc-safety-net)\n[![Version](https://img.shields.io/github/v/tag/kenryu42/cc-safety-net?label=version\u0026color=blue)](https://github.com/kenryu42/cc-safety-net)\n[![Codex](https://img.shields.io/badge/Codex-white)](#codex-installation)\n[![Claude Code](https://img.shields.io/badge/Claude%20Code-D27656)](#claude-code-installation)\n[![Copilot CLI](https://img.shields.io/badge/Copilot%20CLI-4EA5C9)](#github-copilot-cli-installation)\n[![Gemini CLI](https://img.shields.io/badge/Gemini%20CLI-678AE3)](#gemini-cli-installation)\n[![Kimi Code](https://img.shields.io/badge/Kimi%20Code-5587FF)](#kimi-code-installation)\n[![OpenCode](https://img.shields.io/badge/OpenCode-black)](#opencode-installation)\n[![Pi](https://img.shields.io/badge/Pi%20Coding-22262E)](#pi-installation)\n[![License: MIT](https://img.shields.io/badge/License-MIT-red.svg)](https://opensource.org/licenses/MIT)\n\n\u003cdiv align=\"center\"\u003e\n\n[![CC Safety Net](./.github/assets/cc-safety-net.png)](./.github/assets/cc-safety-net.png)\n\n\u003c/div\u003e\n\nA Coding Agent CLI plugin that acts as a safety net, catching destructive git and filesystem commands before they execute.\n\n## Contents\n\n- [Why This Exists](#why-this-exists)\n- [Why Use This Instead of Permission Deny Rules?](#why-use-this-instead-of-permission-deny-rules)\n- [What About Sandboxing?](#what-about-sandboxing)\n- [Prerequisites](#prerequisites)\n- [Quick Start](#quick-start)\n  - [Codex Installation](#codex-installation)\n  - [Claude Code Installation](#claude-code-installation)\n  - [Gemini CLI Installation](#gemini-cli-installation)\n  - [GitHub Copilot CLI Installation](#github-copilot-cli-installation)\n  - [Kimi Code Installation](#kimi-code-installation)\n  - [OpenCode Installation](#opencode-installation)\n  - [Pi Installation](#pi-installation)\n- [Status Line Integration](#status-line-integration)\n  - [Emoji Mode Indicators](#emoji-mode-indicators)\n- [Diagnostics](#diagnostics)\n- [Explain (Debug Analysis)](#explain-debug-analysis)\n- [Commands Blocked](#commands-blocked)\n- [Commands Allowed](#commands-allowed)\n- [What Happens When Blocked](#what-happens-when-blocked)\n- [Testing the Hook](#testing-the-hook)\n- [Breaking Change: Custom Rules Migration](#breaking-change-custom-rules-migration)\n- [Custom Rules](#custom-rules)\n  - [Config File Location](#config-file-location)\n  - [Rule Schema](#rule-schema)\n  - [Matching Behavior](#matching-behavior)\n  - [Rule Examples](#rule-examples)\n  - [Error Handling](#error-handling)\n- [Advanced Features](#advanced-features)\n  - [Strict Mode](#strict-mode)\n  - [Paranoid Mode](#paranoid-mode)\n  - [Worktree Mode](#worktree-mode)\n  - [Shell Wrapper Detection](#shell-wrapper-detection)\n  - [Interpreter One-Liner Detection](#interpreter-one-liner-detection)\n  - [Secret Redaction](#secret-redaction)\n  - [Audit Logging](#audit-logging)\n- [Development](#development)\n- [License](#license)\n\n## Why This Exists\n\nWe learned the [hard way](https://www.reddit.com/r/ClaudeAI/comments/1pgxckk/claude_cli_deleted_my_entire_home_directory_wiped/) that instructions aren't enough to keep AI agents in check.\nAfter 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.\nThe current approach is to use a dedicated hook to programmatically prevent agents from running destructive commands.\n\n## Why Use This Instead of Permission Deny Rules?\n\nClaude Code's `.claude/settings.json` supports [deny rules](https://code.claude.com/docs/en/iam#tool-specific-permission-rules) with wildcard matching (e.g., `Bash(git reset --hard:*)`). Here's how this plugin differs:\n\n### At a Glance\n\n| | Permission Deny Rules | CC Safety Net |\n|---|---|---|\n| **Setup** | Manual configuration required | Works out of the box |\n| **Parsing** | Wildcard pattern matching | Semantic command analysis |\n| **Execution order** | Runs second | Runs first (PreToolUse hook) |\n| **Shell wrappers** | Not handled automatically (must match wrapper forms) | Recursively analyzed (up to 10 levels) |\n| **Interpreter one-liners** | Not handled automatically (must match interpreter forms) | Detected and blocked |\n\n### Permission Rules Have Known Bypass Vectors\n\nEven with wildcard matching, Bash permission patterns are intentionally limited and can be bypassed in many ways:\n\n| Bypass Method | Example |\n|---------------|---------|\n| Options before value | `curl -X GET http://evil.com` bypasses `Bash(curl http://evil.com:*)` |\n| Shell variables | `URL=http://evil.com \u0026\u0026 curl $URL` bypasses URL pattern |\n| Flag reordering | `rm -r -f /` bypasses `Bash(rm -rf:*)` |\n| Extra whitespace | `rm  -rf /` (double space) bypasses pattern |\n| Shell wrappers | `sh -c \"rm -rf /\"` bypasses `Bash(rm:*)` entirely |\n\n### CC Safety Net Handles What Patterns Can't\n\n| Scenario | Permission Rules | CC Safety Net |\n|----------|------------------|------------|\n| `git checkout -b feature` (safe) | Blocked by `Bash(git checkout:*)` | Allowed |\n| `git checkout -- file` (dangerous) | Blocked by `Bash(git checkout:*)` | Blocked |\n| `rm -rf /tmp/cache` (safe) | Blocked by `Bash(rm -rf:*)` | Allowed |\n| `rm -r -f /` (dangerous) | Allowed (flag order) | Blocked |\n| `bash -c 'git reset --hard'` | Allowed (wrapper) | Blocked |\n| `python -c 'os.system(\"rm -rf /\")'` | Allowed (interpreter) | Blocked |\n\n### Defense in Depth\n\nPreToolUse hooks run [**before**](https://code.claude.com/docs/en/iam#additional-permission-control-with-hooks) the permission system. This means CC Safety Net inspects every command first, regardless of your permission configuration. Even if you misconfigure deny rules, CC Safety Net provides a fallback layer of protection.\n\n**Use both together**: Permission deny rules for quick, user-configurable blocks; CC Safety Net for robust, bypass-resistant protection that works out of the box.\n\n## What About Sandboxing?\n\nClaude Code offers [native sandboxing](https://code.claude.com/docs/en/sandboxing) that provides OS-level filesystem and network isolation. Here's how it compares to CC Safety Net:\n\n### Different Layers of Protection\n\n| | Sandboxing | CC Safety Net |\n|---|---|---|\n| **Enforcement** | OS-level (Seatbelt/bubblewrap) | Application-level (PreToolUse hook) |\n| **Approach** | Containment — restricts filesystem + network access | Command analysis — blocks destructive operations |\n| **Filesystem** | Writes restricted (default: cwd); reads are broad by default | Only destructive operations blocked |\n| **Network** | Domain-based proxy filtering | None |\n| **Git awareness** | None | Explicit rules for destructive git operations |\n| **Bypass resistance** | High — OS enforces boundaries | Lower — analyzes command strings only |\n\n### Why Sandboxing Isn't Enough\n\nSandboxing 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:\n\n\u003e [!NOTE]\n\u003e 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`.\n\n| Command | Sandboxing | CC Safety Net |\n|---------|------------|------------|\n| `git reset --hard` | Allowed (within cwd) | **Blocked** |\n| `git checkout -- .` | Allowed (within cwd) | **Blocked** |\n| `git stash clear` | Allowed (within cwd) | **Blocked** |\n| `git push --force` | Allowed (if remote domain is allowed) | **Blocked** |\n| `rm -rf .` | Allowed (within cwd) | **Blocked** |\n\nSandboxing sees `git reset --hard` as a safe operation—it only modifies files within the current directory. But you just lost all uncommitted work.\n\n### When to Use Sandboxing Instead\n\nSandboxing is the better choice when your primary concern is:\n\n- **Prompt injection attacks** — Reduces exfiltration risk by restricting outbound domains (depends on your allowed-domain policy)\n- **Malicious dependencies** — Limits filesystem writes and network access by default (subject to your sandbox configuration)\n- **Untrusted code execution** — OS-level containment is stronger than pattern matching\n- **Network control** — CC Safety Net has no network protection\n\n### Recommended: Use Both\n\nThey protect against different threats:\n\n- **Sandboxing** contains blast radius — even if something goes wrong, damage is limited to cwd and approved network domains\n- **CC Safety Net** prevents footguns — catches git-specific mistakes that are technically \"safe\" from the sandbox's perspective\n\nRunning both together provides defense-in-depth. Sandboxing handles unknown threats; CC Safety Net handles known destructive patterns that sandboxing permits.\n\n## Prerequisites\n\n- **Node.js**: Version 18 or higher is required to run this plugin\n\n## Quick Start\n\n### Codex Installation\n\n1. Enable Codex plugin hooks in `~/.codex/config.toml`:\n\n  ```toml\n  [features]\n  plugin_hooks = true\n  ```\n\n2. Add the marketplace:\n\n  ```bash\n  codex plugin marketplace add kenryu42/cc-marketplace\n  ```\n\n3. Start Codex.\n4. In the TUI, run `/plugins`.\n5. Use arrow keys to select `[cc-marketplace]`.\n6. Press Enter to install the plugin.\n7. run `/hooks` and select the safety-net PreToolUse hook and press `t` to trust it.\n\n---\n\n### Claude Code Installation\n\n```bash\n/plugin marketplace add kenryu42/cc-marketplace\n/plugin install safety-net@cc-marketplace\n/reload-plugins\n```\n\n### Claude Code Auto-Update\n\n1. Run `/plugin` → Select `Marketplaces` → Choose `cc-marketplace` → Enable auto-update\n\n---\n\n### Gemini CLI Installation\n\n```bash\ngemini extensions install https://github.com/kenryu42/gemini-safety-net\n```\n\n---\n\n### GitHub Copilot CLI Installation\n\n```bash\n/plugin install kenryu42/copilot-safety-net\n```\n\n---\n\n### Kimi Code Installation\n\nInstall CC Safety Net into your Kimi Code config:\n\n```bash\nnpx -y cc-safety-net hook install --kimi-code\n```\n\nOptional: run `npx skill add kenryu42/cc-safety-net` to add the `/cc-safety-net` skill for configuring custom rules.\n\n---\n\n\n### OpenCode Installation\n\nInstall CC Safety Net with OpenCode's native plugin command:\n\n```bash\nopencode plugin -g cc-safety-net\n```\n\n\u003e [!NOTE]\n\u003e OpenCode can sometimes keep using a stale cached plugin version. See\n\u003e anomalyco/opencode#25293 for the current tracking issue.\n\u003e\n\u003e To force OpenCode to reinstall `cc-safety-net`, remove its cached package and\n\u003e install the version you want:\n\u003e\n\u003e ```sh\n\u003e rm -rf ~/.cache/opencode/packages/cc-safety-net@latest\n\u003e opencode plugin -g -f cc-safety-net@latest\n\u003e\n\u003e If you prefer pinning a specific version:\n\u003e\n\u003e rm -rf ~/.cache/opencode/packages/cc-safety-net@latest\n\u003e opencode plugin -g -f cc-safety-net@\u003cversion\u003e\n\u003e\n\u003e Restart OpenCode after updating so the plugin is loaded from the refreshed\n\u003e cache.\n\n---\n\n### Pi Installation\n\nInstall CC Safety Net with Pi's package installer:\n\n```bash\npi install npm:cc-safety-net\n```\n\n---\n\n## Status Line Integration\n\nCC Safety Net can display its status in Claude Code's status line, showing whether protection is active and which modes are enabled.\n\nAdd the following to your `~/.claude/settings.json`:\n\n**Using Bun (recommended):**\n\n```json\n{\n  \"statusLine\": {\n    \"type\": \"command\",\n    \"command\": \"bunx cc-safety-net statusline --claude-code\"\n  }\n}\n```\n\n**Using Claude X:**\n\n```json\n{\n  \"statusLine\": {\n    \"type\": \"command\",\n    \"command\": \"BUN_BE_BUN=1 claude x cc-safety-net statusline --claude-code\"\n  }\n}\n```\n\u003e [!NOTE]\n\u003e The `claude x` command is only compatible with the native version of Claude Code. If you installed via npm, please use `npx` or `bunx` instead.\n\n\n\n**Using NPM:**\n\n```json\n{\n  \"statusLine\": {\n    \"type\": \"command\",\n    \"command\": \"npx -y cc-safety-net statusline --claude-code\"\n  }\n}\n```\n\n**Piping with existing status line:**\n\nIf you already have a status line command, you can pipe CC Safety Net at the end:\n\n```json\n{\n  \"statusLine\": {\n    \"type\": \"command\",\n    \"command\": \"your-existing-command | bunx cc-safety-net statusline --claude-code\"\n  }\n}\n```\n\nChanges take effect immediately — no restart needed.\n\n### Emoji Mode Indicators\n\nThe status line displays different emojis based on the current configuration:\n\n| Status | Display | Meaning |\n|--------|---------|---------|\n| Plugin disabled | `🛡️ CC Safety Net ❌` | CC Safety Net plugin is not enabled |\n| Default mode | `🛡️ CC Safety Net ✅` | Protection active with default settings |\n| Strict mode | `🛡️ CC Safety Net 🔒` | `CC_SAFETY_NET_STRICT=1` — fail-closed on unparseable commands |\n| Paranoid mode | `🛡️ CC Safety Net 👁️` | `CC_SAFETY_NET_PARANOID=1` — all paranoid checks enabled |\n| Paranoid RM only | `🛡️ CC Safety Net 🗑️` | `CC_SAFETY_NET_PARANOID_RM=1` — blocks `rm -rf` even within cwd |\n| Paranoid interpreters only | `🛡️ CC Safety Net 🐚` | `CC_SAFETY_NET_PARANOID_INTERPRETERS=1` — blocks interpreter one-liners |\n| Worktree mode | `🛡️ CC Safety Net 🌳` | `CC_SAFETY_NET_WORKTREE=1` — relax local git discards inside linked worktrees |\n| Strict + Paranoid | `🛡️ CC Safety Net 🔒👁️` | Both strict and paranoid modes enabled |\n\nMultiple mode emojis are combined when multiple environment variables are set. Mode flags use `CC_SAFETY_NET_*` names; legacy `SAFETY_NET_*` names are still accepted.\n\n## Diagnostics\n\nRun the diagnostic command to verify your installation and troubleshoot issues:\n\n```bash\nnpx cc-safety-net doctor\n# or with bun\nbunx cc-safety-net doctor\n```\n\nThe doctor command checks:\n\n| Check | Description |\n|-------|-------------|\n| Hook Integration | Verifies the plugin is properly configured for each supported platform |\n| Self-Test | Runs sample commands to confirm blocking works correctly |\n| Configuration | Validates custom rules in user and project configs |\n| Environment | Shows status of mode flags (`CC_SAFETY_NET_STRICT`, `CC_SAFETY_NET_PARANOID`, etc.; legacy `SAFETY_NET_*` also listed when set) |\n| Recent Activity | Summarizes blocked commands from the last 7 days |\n| System Info | Displays versions of all relevant tools |\n| Update Check | Checks if a newer version is available |\n\n### Options\n\n| Flag | Description |\n|------|-------------|\n| `--json` | Output in JSON format (useful for sharing in bug reports) |\n| `--skip-update-check` | Skip the npm version check |\n\n## Explain (Debug Analysis)\n\nTrace how CC Safety Net analyzes a command step-by-step. Useful for debugging why a command is blocked or allowed, or when developing custom rules.\n\n```bash\nnpx cc-safety-net explain \"git reset --hard\"\n# or with bun\nbunx cc-safety-net explain \"git reset --hard\"\n```\n\n### Options\n\n| Flag | Description |\n|------|-------------|\n| `--json` | Output analysis as JSON |\n| `--cwd \u003cpath\u003e` | Use custom working directory for analysis |\n\n### Examples\n\n```bash\nnpx cc-safety-net explain \"rm -rf /\"\nnpx cc-safety-net explain --json \"git checkout -- file.txt\"\nnpx cc-safety-net explain --cwd /tmp \"git status\"\n```\n\n## Commands Blocked\n\n| Command Pattern | Why It's Dangerous |\n|-----------------|-------------------|\n| git checkout -- files | Discards uncommitted changes permanently |\n| git checkout \\\u003cref\\\u003e -- \\\u003cpath\\\u003e | Overwrites working tree with ref version |\n| git checkout \\\u003cref\\\u003e \\\u003cpath\\\u003e | May overwrite working tree when Git disambiguates ref vs pathspec |\n| git restore files | Discards uncommitted changes |\n| git restore --worktree | Explicitly discards working tree changes |\n| git switch --discard-changes | Discards uncommitted changes when switching branches |\n| git switch --force / -f | Discards uncommitted changes (force switch) |\n| git reset --hard | Destroys all uncommitted changes |\n| git reset --merge | Can lose uncommitted changes |\n| git clean -f | Removes untracked files permanently |\n| git push --force / -f | Destroys remote history |\n| git branch -D | Force-deletes branch without merge check |\n| git stash drop | Permanently deletes stashed changes |\n| git stash clear | Deletes ALL stashed changes |\n| git worktree remove --force | Force-deletes worktree without checking for changes |\n| rm -rf (destructive targets) | Recursive file deletion of root, home, parent, absolute, or non-temp paths outside cwd |\n| rm -rf / or ~ or $HOME | Root/home deletion is extremely dangerous |\n| find ... -delete | Permanently removes files matching criteria |\n| xargs rm -rf | Dynamic input makes targets unpredictable |\n| xargs \\\u003cshell\\\u003e -c | Can execute arbitrary commands |\n| parallel rm -rf | Dynamic input makes targets unpredictable |\n| parallel \\\u003cshell\\\u003e -c | Can execute arbitrary commands |\n| dd writing to block devices | Can overwrite disks or partitions |\n| mkfs on block devices | Formats disks or partitions |\n| shred | Permanently destroys file contents |\n\n## Commands Allowed\n\n| Command Pattern | Why It's Safe |\n|-----------------|--------------|\n| git checkout -b branch | Creates new branch |\n| git checkout --orphan | Creates orphan branch |\n| git restore --staged | Only unstages, doesn't discard |\n| git restore --help/--version | Help/version output |\n| git branch -d | Safe delete with merge check |\n| git clean -n / --dry-run | Preview only |\n| git push --force-with-lease | Safe force push |\n| rm -rf /tmp/... | Temp directories are ephemeral |\n| rm -rf /var/tmp/... | System temp directory |\n| rm -rf $TMPDIR/... | User's temp directory |\n| rm -rf ./... (within cwd) | Limited to current working directory |\n| git restore / checkout -- / reset --hard / clean -f (in linked worktree) | Relaxed only when `CC_SAFETY_NET_WORKTREE=1` and cwd is a linked worktree |\n\n## What Happens When Blocked\n\nWhen a destructive command is detected, the plugin blocks the tool execution and provides a reason.\n\nExample output:\n```text\nBLOCKED by CC Safety Net\n\nReason: git checkout -- discards uncommitted changes permanently. Use 'git stash' first.\n\nCommand: git checkout -- src/main.py\n\nIf this operation is truly needed, ask the user for explicit permission and have them run the command manually.\n```\n\n## Testing the Hook\n\nYou can manually test the hook by attempting to run blocked commands in Claude Code:\n\n```bash\n# This should be blocked\ngit checkout -- README.md\n\n# This should be allowed\ngit checkout -b test-branch\n```\n\n## Breaking Change: Custom Rules Migration\n\n\u003e [!WARNING]\n\u003e The custom rules system has moved from legacy inline config files to a rulebook-based layout. Legacy inline config files (`.safety-net.json` and `~/.cc-safety-net/config.json`) are **no longer loaded at runtime**. If they contain rules, commands now **fail closed** (stay blocked) until you migrate.\n\n### Who Is Affected\n\n- **Affected**: users who previously defined custom rules in `.safety-net.json` (project scope) or `~/.cc-safety-net/config.json` (user scope).\n- **Not affected**: users with no custom rules. All built-in destructive-command protections are unchanged and continue to work out of the box.\n\n### What Breaks\n\n| Legacy file state | New behavior |\n|-------------------|--------------|\n| Empty legacy file | Silently ignored — built-in rules only |\n| Legacy file with rules | Fail closed until migrated with `rule migrate` |\n| Invalid legacy file | Fail closed until fixed and migrated, or removed |\n\n\"Fail closed\" means commands stay blocked until the legacy rules are migrated to the new layout.\n\n### How to Migrate\n\n```bash\n# Convert legacy inline rules into the new rulebook layout\nnpx -y cc-safety-net rule migrate\n\n# Optionally delete verified legacy files after migration\nnpx -y cc-safety-net rule migrate --cleanup\n\n# Validate the migrated rules\nnpx -y cc-safety-net rule verify\nnpx -y cc-safety-net rule test\n```\n\n### Before / After\n\n**Before** — a single inline config with rules embedded:\n\n```text\n.safety-net.json          # project rules (inline)\n~/.cc-safety-net/config.json   # user rules (inline)\n```\n\n**After** — `rule migrate` creates a rulebook-based layout automatically:\n\n```text\n.cc-safety-net/rules/rule.json                    # project rulebook sources + overrides\n.cc-safety-net/rules/project-rules/rulebook.json  # migrated project rules\n~/.cc-safety-net/rules/rule.json                  # user rulebook sources + overrides\n~/.cc-safety-net/rules/user-rules/rulebook.json   # migrated user rules\n```\n\nSee [Custom Rules](#custom-rules) for the full authoring guide and [Error Handling](#error-handling) for fail-closed details.\n\n## Custom Rules\n\nBeyond the built-in protections, you can define your own blocking rules to enforce team conventions or project-specific safety policies.\n\n\u003e [!TIP]\n\u003e The best way to create custom rules is to use the `/cc-safety-net` skill to create custom rules interactively with natural language.\n\n### Examples\n\n```\n/cc-safety-net read my package.json and suggest blocking rules\n/cc-safety-net set up rules to block all terraform destroy commands\n/cc-safety-net verify my rules and fix any errors\n```\n\n\u003e [!NOTE]\n\u003e If your agent does not support skills, prompt it with:\n\u003e ```\n\u003e run npx -y cc-safety-net rule doc and help me set up custom rules\n\u003e ```\n\n### Create Rules Manually\n\nCreate a starter project rule config and rulebook:\n\n```bash\nnpx -y cc-safety-net rule init\n```\n\nThis creates `.cc-safety-net/rules/rule.json`:\n\n```json\n{\n  \"version\": 1,\n  \"rules\": [\"project-rules\"],\n  \"overrides\": {}\n}\n```\n\nRule definitions live in `.cc-safety-net/rules/project-rules/rulebook.json`:\n\n```json\n{\n  \"rulebook_version\": 1,\n  \"name\": \"project-rules\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Project-specific CC Safety Net rules.\",\n  \"author\": \"project\",\n  \"allowed_commands\": [\"git\"],\n  \"rules\": [\n    {\n      \"name\": \"block-git-add-all\",\n      \"command\": \"git\",\n      \"subcommand\": \"add\",\n      \"block_args\": [\"-A\", \"--all\", \".\"],\n      \"reason\": \"Use 'git add \u003cspecific-files\u003e' instead of blanket add.\"\n    }\n  ],\n  \"tests\": [\n    {\n      \"command\": \"git add -A\",\n      \"expect\": \"blocked\",\n      \"rule\": \"block-git-add-all\"\n    },\n    {\n      \"command\": \"git add README.md\",\n      \"expect\": \"allowed\"\n    }\n  ]\n}\n```\n\nAfter editing rulebooks, run:\n\n```bash\nnpx -y cc-safety-net rule sync\nnpx -y cc-safety-net rule verify\nnpx -y cc-safety-net rule test\n```\n\nNow `git add -A`, `git add --all`, and `git add .` will be blocked with your custom message.\n\n### Config File Location\n\nConfig files are loaded from two scopes and merged:\n\n1. **User scope**: `~/.cc-safety-net/rules/rule.json` (use `rule init --global`)\n2. **Project scope**: `.cc-safety-net/rules/rule.json` in the current working directory\n\nLocal rulebook sources are bare names like `project-rules`. GitHub rulebook sources use `owner/repo#ref/\u003crulebook-name\u003e` and point to `.cc-safety-net/rules/\u003crulebook-name\u003e/rulebook.json` in that repository.\n\nLegacy inline config files (`.safety-net.json` and `~/.cc-safety-net/config.json`) are no longer loaded at runtime. Empty legacy files are ignored, but legacy files with rules and invalid legacy files fail closed until migrated or fixed. Convert existing legacy rules with `npx -y cc-safety-net rule migrate`; use `npx -y cc-safety-net rule migrate --cleanup` if you also want to delete verified legacy files after migration. See [Breaking Change: Custom Rules Migration](#breaking-change-custom-rules-migration) for the full upgrade guide.\n\n**Merging behavior**:\n- Rulebooks from both scopes are combined\n- Duplicate active rulebook names are invalid\n- Project overrides win over user overrides for the same `\u003crulebook-name\u003e/\u003crule-name\u003e` key\n\nThis allows you to define personal defaults in user scope while letting projects disable or replace reasons for specific rules.\n\nIf no config file is found in either location, only built-in rules apply.\n\n### Config Schema\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `version` | integer | Yes | Schema version (must be `1`) |\n| `rules` | array | No | List of rulebook source strings (defaults to empty) |\n| `overrides` | object | No | Rule overrides keyed by `\u003crulebook-name\u003e/\u003crule-name\u003e` |\n\nOverride values are either `\"off\"` to disable a rule or `{ \"reason\": \"...\" }` to replace the rule reason.\n\n### Rulebook Schema\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `rulebook_version` | integer | Yes | Rulebook schema version (must be `1`) |\n| `name` | string | Yes | Rulebook name; must match the local directory name or GitHub source name |\n| `version` | string | Yes | Rulebook version |\n| `description` | string | No | Human-readable description |\n| `author` | string | No | Rulebook author |\n| `allowed_commands` | array | Yes | Commands this rulebook is allowed to define rules for |\n| `rules` | array | Yes | Custom blocking rules |\n| `tests` | array | Yes | Rulebook fixtures |\n\n### Rule Schema\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `name` | string | Yes | Unique within the rulebook (letters, numbers, hyphens, underscores; max 64 chars) |\n| `command` | string | Yes | Base command to match; must be listed in `allowed_commands` |\n| `subcommand` | string | No | Subcommand to match (e.g., `add`, `install`). If omitted, matches any. |\n| `block_args` | array | Yes | Arguments that trigger the block (at least one required) |\n| `reason` | string | Yes | Message shown when blocked (max 256 chars) |\n\n### Fixture Schema\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `command` | string | Yes | Shell command fixture |\n| `expect` | string | Yes | Either `blocked` or `allowed` |\n| `rule` | string | For blocked fixtures | Rule expected to block the command |\n\nEvery rule must have at least one blocked fixture. Add allowed fixtures for close-but-safe commands.\n\n### Matching Behavior\n\n- **Commands** are normalized to basename (`/usr/bin/git` → `git`)\n- **Subcommand** is the first non-option argument after the command\n- **Arguments** are matched literally (no regex, no glob), with short option expansion\n- A command is blocked if **any** argument in `block_args` is present\n- **Short options** are expanded: `-Ap` matches `-A` (bundled flags are unbundled)\n- **Long options** use exact match: `--all-files` does NOT match `--all`\n- Custom rules only add restrictions—they cannot bypass built-in protections\n\n#### Known Limitations\n\n- **Short option expansion**: `-Cfoo` is treated as `-C -f -o -o`, not `-C foo`. Blocking `-f` may false-positive on attached option values.\n\n### Rule Examples\n\n#### Block global npm installs\n\n`.cc-safety-net/rules/rule.json`:\n\n```json\n{\n  \"version\": 1,\n  \"rules\": [\"project-rules\"],\n  \"overrides\": {}\n}\n```\n\n`.cc-safety-net/rules/project-rules/rulebook.json`:\n\n```json\n{\n  \"rulebook_version\": 1,\n  \"name\": \"project-rules\",\n  \"version\": \"1.0.0\",\n  \"allowed_commands\": [\"npm\"],\n  \"rules\": [\n    {\n      \"name\": \"block-npm-global\",\n      \"command\": \"npm\",\n      \"subcommand\": \"install\",\n      \"block_args\": [\"-g\", \"--global\"],\n      \"reason\": \"Global npm installs can cause version conflicts. Use npx or local install.\"\n    }\n  ],\n  \"tests\": [\n    {\n      \"command\": \"npm install -g typescript\",\n      \"expect\": \"blocked\",\n      \"rule\": \"block-npm-global\"\n    },\n    {\n      \"command\": \"npm install typescript\",\n      \"expect\": \"allowed\"\n    }\n  ]\n}\n```\n\n#### Block dangerous docker commands\n\n```json\n{\n  \"rulebook_version\": 1,\n  \"name\": \"project-rules\",\n  \"version\": \"1.0.0\",\n  \"allowed_commands\": [\"docker\"],\n  \"rules\": [\n    {\n      \"name\": \"block-docker-system-prune\",\n      \"command\": \"docker\",\n      \"subcommand\": \"system\",\n      \"block_args\": [\"prune\"],\n      \"reason\": \"docker system prune removes all unused data. Use targeted cleanup instead.\"\n    }\n  ],\n  \"tests\": [\n    {\n      \"command\": \"docker system prune\",\n      \"expect\": \"blocked\",\n      \"rule\": \"block-docker-system-prune\"\n    },\n    {\n      \"command\": \"docker ps\",\n      \"expect\": \"allowed\"\n    }\n  ]\n}\n```\n\n#### Multiple rules\n\n```json\n{\n  \"rulebook_version\": 1,\n  \"name\": \"project-rules\",\n  \"version\": \"1.0.0\",\n  \"allowed_commands\": [\"git\", \"npm\"],\n  \"rules\": [\n    {\n      \"name\": \"block-git-add-all\",\n      \"command\": \"git\",\n      \"subcommand\": \"add\",\n      \"block_args\": [\"-A\", \"--all\", \".\", \"-u\", \"--update\"],\n      \"reason\": \"Use 'git add \u003cspecific-files\u003e' instead of blanket add.\"\n    },\n    {\n      \"name\": \"block-npm-global\",\n      \"command\": \"npm\",\n      \"subcommand\": \"install\",\n      \"block_args\": [\"-g\", \"--global\"],\n      \"reason\": \"Use npx or local install instead of global.\"\n    }\n  ],\n  \"tests\": [\n    {\n      \"command\": \"git add -A\",\n      \"expect\": \"blocked\",\n      \"rule\": \"block-git-add-all\"\n    },\n    {\n      \"command\": \"npm install -g typescript\",\n      \"expect\": \"blocked\",\n      \"rule\": \"block-npm-global\"\n    }\n  ]\n}\n```\n\n### Error Handling\n\nRulebook-backed custom rules fail closed when configured rulebooks cannot be loaded safely:\n\n| Scenario | Behavior |\n|----------|----------|\n| Config file not found | Silent — use built-in rules only |\n| Invalid rule config | Fail closed until fixed |\n| Empty legacy config | Silent — use built-in rules only |\n| Legacy config with rules and no migrated rule config | Fail closed until `rule migrate` creates the new rule config |\n| Invalid legacy config | Fail closed until fixed or removed |\n| Missing or stale lock/cache | Fail closed until `rule sync` repairs it |\n| Invalid local rulebook | Fail closed until the rulebook is fixed and synced |\n| Invalid GitHub rulebook | Fail closed until the source is fixed or removed |\n\n\n\u003e [!IMPORTANT]  \n\u003e If you add or modify custom rules manually, always validate them with `npx -y cc-safety-net rule verify` and `npx -y cc-safety-net rule test`.\n\n### Block Output Format\n\nWhen a custom rule blocks a command, the output includes the rule name:\n\n```text\nBLOCKED by CC Safety Net\n\nReason: [block-git-add-all] Use 'git add \u003cspecific-files\u003e' instead of blanket add.\n\nCommand: git add -A\n```\n\n## Advanced Features\n\nMode and debug flags use **`CC_SAFETY_NET_*`** environment variables. Older **`SAFETY_NET_*`** names (without the `CC_` prefix) still work for strict, paranoid, and worktree toggles.\n\n### Strict Mode\n\nMalformed or missing hook input JSON always fails closed. By default, ambiguous shell\ncommand parsing is allowed through. Enable strict mode to fail closed when a shell\ncommand cannot be safely analyzed (e.g., unterminated quotes or malformed `bash -c`\nwrappers):\n\n```bash\nexport CC_SAFETY_NET_STRICT=1\n```\n\n### Paranoid Mode\n\nParanoid mode enables stricter safety checks that may be disruptive to normal workflows.\nYou can enable it globally or via focused toggles:\n\n```bash\n# Enable all paranoid checks\nexport CC_SAFETY_NET_PARANOID=1\n\n# Or enable specific paranoid checks\nexport CC_SAFETY_NET_PARANOID_RM=1\nexport CC_SAFETY_NET_PARANOID_INTERPRETERS=1\n```\n\nParanoid behavior:\n\n- **rm**: blocks non-temp `rm -rf` even within the current working directory.\n- **interpreters**: blocks interpreter one-liners like `python -c`, `node -e`, `ruby -e`,\n  and `perl -e` (these can hide destructive commands).\n\n### Worktree Mode\n\nLinked git worktrees are designed as disposable, isolated workspaces — discarding\nchanges inside one doesn't risk the main working tree. Worktree mode relaxes\nlocal-discard rules when (and only when) the command is proven to run inside a\nlinked worktree:\n\n```bash\nexport CC_SAFETY_NET_WORKTREE=1\n```\n\nWhen enabled, these commands are allowed inside a linked worktree:\n\n- `git restore \u003cfile\u003e` and `git restore --worktree \u003cfile\u003e`\n- `git checkout -- \u003cfile\u003e`, `git checkout \u003cref\u003e -- \u003cfile\u003e`, `git checkout --force`,\n  and ambiguous multi-positional checkout forms\n- `git switch --discard-changes` and `git switch -f / --force`\n- `git reset --hard` and `git reset --merge`\n- `git clean -f` (and combined short flags like `-fd`)\n\nThese remain blocked even in linked worktrees because they reach beyond the\nlocal working tree:\n\n- `git push --force` (affects remote)\n- `git branch -D` (affects shared refs)\n- `git stash drop` / `git stash clear` (stash is shared across worktrees)\n- `git worktree remove --force` (could delete another worktree)\n\nDetection is fail-closed and mostly filesystem-based:\n\n- A linked worktree is identified by a `.git` *file* containing `gitdir:` whose\n  resolved git directory contains a `commondir` file. Main worktrees and\n  submodules don't satisfy this and are not relaxed.\n- The cwd walk uses `realpath` so symlinked paths resolve correctly.\n- `git -C \u003cpath\u003e` (including chained `-C` and attached `-Cpath`) is honored;\n  unresolved targets keep the command blocked.\n- Relaxation is disabled if cwd becomes unknown (e.g., after `cd`/`pushd`),\n  if `--git-dir` / `--work-tree` is passed, or if `GIT_DIR` / `GIT_WORK_TREE`\n  / `GIT_COMMON_DIR` is set in the environment.\n- Git may be invoked from a trusted system path to inspect effective config that\n  could make submodule operations recursive.\n\n### Shell Wrapper Detection\n\nThe guard recursively analyzes commands wrapped in shells:\n\n```bash\nbash -c 'git reset --hard'    # Blocked\nsh -lc 'rm -rf /'             # Blocked\nbash -c 'git stash drop'      # Blocked\n```\n\n### Interpreter One-Liner Detection\n\nDetects destructive commands hidden in Python/Node/Ruby/Perl one-liners:\n\n```bash\npython -c 'import os; os.system(\"rm -rf /\")'  # Blocked\npython -c 'import os; os.system(\"git stash drop\")'  # Blocked\npython -c 'import os; os.system(\"dd if=/dev/zero of=/dev/sda\")'  # Blocked\npython -c 'import os; os.system(\"mkfs.ext4 /dev/sda1\")'  # Blocked\npython -c 'import os; os.system(\"shred -u secret.txt\")'  # Blocked\n```\n\n### Secret Redaction\n\nBlock messages automatically redact sensitive data (tokens, passwords, API keys) to prevent leaking secrets in logs.\n\n### Audit Logging\n\nAll blocked commands are logged to `~/.cc-safety-net/logs/\u003csession_id\u003e.jsonl` for audit purposes:\n\n```json\n{\"ts\": \"2025-01-15T10:30:00Z\", \"command\": \"git reset --hard\", \"segment\": \"git reset --hard\", \"reason\": \"...\", \"cwd\": \"/path/to/project\"}\n```\n\nSensitive data in log entries is automatically redacted.\n\n## Development\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this project.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkenryu42%2Fcc-safety-net","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkenryu42%2Fcc-safety-net","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkenryu42%2Fcc-safety-net/lists"}