> ## Documentation Index
> Fetch the complete documentation index at: https://ccsafetynet.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Rules: Extra Command Argument Patterns

> Define project and user-level custom blocking rules in CC Safety Net using JSON config files. Block commands by matching command, subcommand, and literal argument patterns with a custom reason message.

Beyond built-in protections, CC Safety Net lets you define custom blocking rules to enforce team conventions or project-specific safety policies. Rules live in a **rulebook-based layout** and are merged from user and project scopes, so you can maintain personal defaults alongside per-project overrides without duplication.

<Warning>
  **Breaking change** — 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. Run `npx -y cc-safety-net rule migrate` to convert legacy rules into the new rulebook layout. See [Migration](#migration-from-legacy-config) below.
</Warning>

<Tip>
  The fastest way to author custom rules is to use the **`/cc-safety-net` skill** inside your agent to create rules interactively with natural language. Examples:

  ```text theme={"dark"}
  /cc-safety-net read my package.json and suggest blocking rules
  /cc-safety-net set up rules to block all terraform destroy commands
  /cc-safety-net verify my rules and fix any errors
  ```

  If your agent does not support skills, prompt it with:

  ```text theme={"dark"}
  run npx -y cc-safety-net rule doc and help me set up custom rules
  ```
</Tip>

## Config File Locations

CC Safety Net loads rulebooks from two scopes and merges them:

1. **User scope** — `~/.cc-safety-net/rules/rule.json` (created with `rule init --global`). Use this for personal defaults that apply to every project.
2. **Project scope** — `.cc-safety-net/rules/rule.json` in the project root. Use this for team or project-specific rules you can commit to source control.

Local rulebook sources are referenced by bare names like `project-rules`. GitHub rulebook sources use `owner/repo#ref/<rulebook-name>` and point to `.cc-safety-net/rules/<rulebook-name>/rulebook.json` in that repository.

**Merging behavior:**

* Rulebooks from both scopes are combined.
* Duplicate active rulebook names are invalid.
* Project overrides win over user overrides for the same `<rulebook-name>/<rule-name>` key.

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

## Managing rulebook sources

A rulebook source is referenced by an entry in `rule.json`'s `rules` array. There are two kinds:

* **Local source** — a bare name like `project-rules`. The rulebook lives at `.cc-safety-net/rules/project-rules/rulebook.json` (project) or `~/.cc-safety-net/rules/project-rules/rulebook.json` (user). Local sources must stay within their config directory.
* **GitHub source** — `owner/repo#ref/<rulebook-name>`, pointing at `.cc-safety-net/rules/<rulebook-name>/rulebook.json` in that repository and ref.

Use the `rule` command to add, update, and remove sources rather than editing `rule.json` and the lockfile by hand:

```bash theme={"dark"}
# Add a local rulebook source and sync
npx -y cc-safety-net rule add project-rules

# Add a GitHub rulebook source (creates a lock entry pinned by SHA-256 digest)
npx -y cc-safety-net rule add kenryu42/cc-safety-net#main/example-rules

# Refresh the lock and cache after editing sources
npx -y cc-safety-net rule sync

# Update a single source, or all sources when no source is given
npx -y cc-safety-net rule update example-rules

# List active rulebooks and their resolved sources
npx -y cc-safety-net rule list

# Remove a source (--delete-source also deletes a local source directory)
npx -y cc-safety-net rule remove example-rules --delete-source
```

Add `--global` (`-g`) to operate on the user scope instead of the project scope.

### Lock and cache

Every configured source is pinned in a `rule.lock` file by a SHA-256 digest, and its rulebook is cached under `.cc-safety-net/cache/rulebooks/`. At runtime CC Safety Net verifies the cached rulebook against the digest. A missing or stale lock/cache, a digest mismatch, or a local source that drifts from its pinned digest causes commands to **fail closed** until you run `rule sync` to repair it.

## Quick Start

Create a starter project rule config and rulebook:

```bash theme={"dark"}
npx -y cc-safety-net rule init
```

This creates `.cc-safety-net/rules/rule.json`:

```json theme={"dark"}
{
  "version": 1,
  "rules": ["project-rules"],
  "overrides": {}
}
```

Rule definitions live in `.cc-safety-net/rules/project-rules/rulebook.json`:

```json theme={"dark"}
{
  "rulebook_version": 1,
  "name": "project-rules",
  "version": "1.0.0",
  "description": "Project-specific CC Safety Net rules.",
  "author": "project",
  "allowed_commands": ["git"],
  "rules": [
    {
      "name": "block-git-add-all",
      "command": "git",
      "subcommand": "add",
      "block_args": ["-A", "--all", "."],
      "reason": "Use 'git add <specific-files>' instead of blanket add."
    }
  ],
  "tests": [
    {
      "command": "git add -A",
      "expect": "blocked",
      "rule": "block-git-add-all"
    },
    {
      "command": "git add README.md",
      "expect": "allowed"
    }
  ]
}
```

After editing rulebooks, run:

```bash theme={"dark"}
npx -y cc-safety-net rule sync
npx -y cc-safety-net rule verify
npx -y cc-safety-net rule test
```

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

## Config Schema

The top-level `rule.json` selects which rulebooks are active and applies overrides.

<ParamField body="version" type="integer" required>
  Schema version. Must be `1`.
</ParamField>

<ParamField body="rules" type="array">
  List of rulebook source strings. Defaults to an empty array.
</ParamField>

<ParamField body="overrides" type="object">
  Rule overrides keyed by `<rulebook-name>/<rule-name>`. Values are either `"off"` to disable a rule or `{ "reason": "..." }` to replace the rule reason.
</ParamField>

## Rulebook Schema

Each rulebook lives in its own `rulebook.json` file.

<ParamField body="rulebook_version" type="integer" required>
  Rulebook schema version. Must be `1`.
</ParamField>

<ParamField body="name" type="string" required>
  Rulebook name. Must match the local directory name or GitHub source name.
</ParamField>

<ParamField body="version" type="string" required>
  Rulebook version string.
</ParamField>

<ParamField body="description" type="string">
  Human-readable description of the rulebook.
</ParamField>

<ParamField body="author" type="string">
  Rulebook author.
</ParamField>

<ParamField body="allowed_commands" type="array" required>
  Commands this rulebook is allowed to define rules for.
</ParamField>

<ParamField body="rules" type="array" required>
  Custom blocking rules (see Rule Schema below).
</ParamField>

<ParamField body="tests" type="array" required>
  Rulebook fixtures (see Fixture Schema below). Every rule must have at least one blocked fixture.
</ParamField>

## Rule Schema

<ParamField body="name" type="string" required>
  Unique within the rulebook. Must start with a letter, followed by letters, numbers, hyphens, or underscores; max 64 chars.
</ParamField>

<ParamField body="command" type="string" required>
  Base command to match. Must be listed in `allowed_commands`.
</ParamField>

<ParamField body="subcommand" type="string">
  Subcommand to match (e.g., `add`, `install`). If omitted, matches any.
</ParamField>

<ParamField body="block_args" type="array" required>
  Arguments that trigger the block (at least one required).
</ParamField>

<ParamField body="reason" type="string" required>
  Message shown when blocked. Max 256 chars.
</ParamField>

## Fixture Schema

<ParamField body="command" type="string" required>
  Shell command fixture.
</ParamField>

<ParamField body="expect" type="string" required>
  Either `blocked` or `allowed`.
</ParamField>

<ParamField body="rule" type="string">
  Rule expected to block the command. Required for blocked fixtures.
</ParamField>

## Matching Behavior

Understanding how CC Safety Net matches commands against your rules helps you write precise, effective rules:

* **Command normalization**: Commands are reduced to their basename before matching. `/usr/local/bin/npm` matches a rule with `"command": "npm"`.
* **Subcommand detection**: The subcommand is the first non-option argument following the command. In `git --no-pager add -A`, the subcommand is `add`.
* **Argument matching**: Arguments in `block_args` are matched literally. No regex or glob support.
* **Short option expansion**: Bundled short flags are unbundled before matching. `-Ap` is treated as `-A` and `-p`.
* **Long option matching**: Long options use exact string matching. `--all-files` does **not** match `--all`.
* **ANY semantics**: A command is blocked if any single argument in `block_args` is present.
* **Additive only**: Custom rules can only add new restrictions. They cannot bypass built-in protections.

<Note>
  **Known limitation**: `-Cfoo` is treated as `-C -f -o -o`, not `-C foo`. Blocking `-f` may false-positive on attached option values.
</Note>

## Examples

<AccordionGroup>
  <Accordion title="Block global npm installs">
    Prevent the agent from installing packages globally:

    ```json theme={"dark"}
    {
      "rulebook_version": 1,
      "name": "project-rules",
      "version": "1.0.0",
      "allowed_commands": ["npm"],
      "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."
        }
      ],
      "tests": [
        {
          "command": "npm install -g typescript",
          "expect": "blocked",
          "rule": "block-npm-global"
        },
        {
          "command": "npm install typescript",
          "expect": "allowed"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Block dangerous docker commands">
    Block `docker system prune`:

    ```json theme={"dark"}
    {
      "rulebook_version": 1,
      "name": "project-rules",
      "version": "1.0.0",
      "allowed_commands": ["docker"],
      "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."
        }
      ],
      "tests": [
        {
          "command": "docker system prune",
          "expect": "blocked",
          "rule": "block-docker-system-prune"
        },
        {
          "command": "docker ps",
          "expect": "allowed"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Multiple rules in one rulebook">
    ```json theme={"dark"}
    {
      "rulebook_version": 1,
      "name": "project-rules",
      "version": "1.0.0",
      "allowed_commands": ["git", "npm"],
      "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."
        }
      ],
      "tests": [
        {
          "command": "git add -A",
          "expect": "blocked",
          "rule": "block-git-add-all"
        },
        {
          "command": "npm install -g typescript",
          "expect": "blocked",
          "rule": "block-npm-global"
        }
      ]
    }
    ```
  </Accordion>
</AccordionGroup>

## Block Output Format

When a custom rule blocks a command, the output is prefixed with the rulebook name and the rule name, so you can tell which rulebook produced the block:

```text theme={"dark"}
BLOCKED by CC Safety Net

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

Command: git add -A
```

The prefix is `<rulebook-name>/<rule-name>`. This is also the key you use in `rule.json` `overrides` to disable a rule (`"off"`) or replace its reason.

## Validating Your Rulebooks

After creating or editing rulebooks, validate them with:

```bash theme={"dark"}
npx -y cc-safety-net rule sync
npx -y cc-safety-net rule verify
npx -y cc-safety-net rule test
```

* `rule sync` rebuilds the lock/cache for configured rulebook sources.
* `rule verify` validates rulebook structure and rule definitions.
* `rule test` runs every fixture in every active rulebook.

## Migration from Legacy Config

Legacy inline config files (`.safety-net.json` and `~/.cc-safety-net/config.json`) are **no longer loaded at runtime**.

| Legacy file state      | New behavior                                     |
| ---------------------- | ------------------------------------------------ |
| Empty legacy file      | Silently ignored — built-in rules only           |
| Legacy file with rules | Fail closed until migrated with `rule migrate`   |
| Invalid legacy file    | Fail closed until fixed and migrated, or removed |

**Fail closed** means commands stay blocked until legacy rules are migrated to the new layout.

```bash theme={"dark"}
# Convert legacy inline rules into the rulebook layout
npx -y cc-safety-net rule migrate

# Optionally delete verified legacy files after migration
npx -y cc-safety-net rule migrate --cleanup

# Validate the migrated rules
npx -y cc-safety-net rule verify
npx -y cc-safety-net rule test
```

**Before** — single inline config with rules embedded:

```text theme={"dark"}
.safety-net.json                # project rules (inline)
~/.cc-safety-net/config.json    # user rules (inline)
```

**After** — `rule migrate` creates a rulebook-based layout automatically:

```text theme={"dark"}
.cc-safety-net/rules/rule.json                    # project rulebook sources + overrides
.cc-safety-net/rules/project-rules/rulebook.json  # migrated project rules
~/.cc-safety-net/rules/rule.json                  # user rulebook sources + overrides
~/.cc-safety-net/rules/user-rules/rulebook.json   # migrated user rules
```

## Error Handling

Rulebook-backed custom rules **fail closed** when configured rulebooks cannot be loaded safely:

| Scenario                                             | Behavior                                                     |
| ---------------------------------------------------- | ------------------------------------------------------------ |
| Config file not found                                | Silent — use built-in rules only                             |
| Invalid rule config                                  | Fail closed until fixed                                      |
| Empty legacy config                                  | Silent — use built-in rules only                             |
| Legacy config with rules and no migrated rule config | Fail closed until `rule migrate` creates the new rule config |
| Invalid legacy config                                | Fail closed until fixed or removed                           |
| Missing or stale lock/cache                          | Fail closed until `rule sync` repairs it                     |
| Invalid local rulebook                               | Fail closed until the rulebook is fixed and synced           |
| Invalid GitHub rulebook                              | Fail closed until the source is fixed or removed             |

<Warning>
  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`.
</Warning>
