Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration

safe-chains integrates with multiple agentic CLI coding tools. List the supported targets with:

safe-chains --list-tools

Install for a specific tool:

safe-chains --setup                   # default: Claude Code
safe-chains --setup --tool=codex      # Codex (OpenAI)
safe-chains --setup --auto-detect     # install for every detected tool

Claude Code

Run safe-chains --setup (or --setup --tool=claude) to automatically configure the hook in ~/.claude/settings.json. Or manually add:

"hooks": {
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "safe-chains"
        }
      ]
    }
  ]
}

Restart your Claude Code sessions to activate the hook. Updating the safe-chains binary takes effect immediately.

Cleaning up approved commands

Once safe-chains is active, most of your existing Bash(...) approved commands in ~/.claude/settings.json and .claude/settings.local.json are redundant. safe-chains already handles them with stricter, flag-level validation.

More importantly, broad patterns can weaken your security. A pattern like Bash(bash *) will approve bash -c "rm -rf /", because Claude Code matches the pattern before safe-chains gets a chance to recursively validate the inner command.

For project-specific scripts or in-house CLIs safe-chains doesn’t ship a definition for, Custom Commands are an alternative to broad Bash(...) approvals: same flag-level validation as built-ins.

Review your approved commands and remove any that safe-chains covers. A good prompt for this:

Find every .claude folder on my system: ~/.claude, any .claude
folders at the top of my projects directory, and .claude folders
inside individual repos. For each settings.json and
settings.local.json, check every Bash(...) pattern against
safe-chains (run `safe-chains "command"` to test). Flag overly
broad patterns like Bash(bash *) or Bash(sh *) that bypass
safe-chains' recursive validation. Present me with a suggested
list of changes for each file before making any edits.

Or, to clear out all approved Bash commands from every Claude settings file at once:

find ~/.claude ~/projects -maxdepth 4 -name 'settings*.json' -path '*/.claude/*' | while read f; do
  jq '
    if .approved_commands then .approved_commands |= map(select(startswith("Bash(") | not)) else . end |
    if .permissions.allow then .permissions.allow |= map(select(startswith("Bash(") | not)) else . end
  ' "$f" > "$f.tmp" && mv "$f.tmp" "$f" && echo "Cleaned $f"
done

This removes every Bash(...) entry but leaves non-Bash permissions (WebFetch, Edit, etc.) untouched.

Codex (OpenAI)

Run safe-chains --setup --tool=codex to write ~/.codex/hooks.json with safe-chains on two hook events. Running it again after an upgrade adds whichever entry is missing. Or manually add to ~/.codex/hooks.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "safe-chains hook codex"
          }
        ]
      }
    ],
    "PermissionRequest": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "safe-chains hook codex"
          }
        ]
      }
    ]
  }
}

The two events do different jobs:

  • PreToolUse runs before every shell command. A command safe-chains recognizes as safe runs as usual; any other command is refused, because inside Codex’s sandbox it would otherwise run without a prompt, and the sandbox still allows reading files anywhere on the machine.
  • PermissionRequest runs only when Codex is about to ask you to approve a command: one that needs to run outside the sandbox or reach the network, or, under the untrusted approval policy, one Codex does not already trust. safe-chains approves a command it recognizes as safe, and leaves every other command to Codex’s own approval prompt.

Codex does not tell the hook which directory a command will run in (a command can name its own working directory). So on PermissionRequest, safe-chains judges each write by how much it depends on that directory, at one of three levels:

  • developer (the default) approves what you would approve in a project: a write to an ordinary relative path (echo x > notes.md, mkdir -p build), a tool’s own build output and edits (cargo build, cargo fmt, git commit -am x) and running the project’s code (cargo run). It leaves to Codex’s prompt any deletion, move or link of a relative path (rm -rf build), any path that climbs out of the directory (../x), and any name that means something wherever it lands: a dotfile, a git hook, a key file or a startup folder (.zshrc, .git/hooks/pre-commit, authorized_keys, Library/LaunchAgents).
  • reads approves only commands that read. Every write goes to Codex’s prompt.
  • workspace treats the directory as the project: it also approves relative deletions and writes into a neighbouring project (../lib/x). It never approves a sensitive name either.

See Safety Levels if you want a level besides developer.

A command approved through PermissionRequest runs outside the sandbox. That includes commands that run the project’s own code, such as cargo test, and commands that read from the network, such as curl -s https://example.com or git fetch, which can carry anything the agent has already read to the address it names. If you want the sandbox to hold for those, leave the PermissionRequest entry out.

PermissionRequest needs Codex 0.122.0 or later. Older versions ignore that entry and keep using PreToolUse.

Codex runs a new or changed hook only after you have trusted it. After installing, open /hooks in Codex, review the safe-chains entries and trust them. Until then Codex skips them without saying so.

Hooks are on by default from Codex 0.124.0. On older versions add this to ~/.codex/config.toml:

[features]
codex_hooks = true

If ~/.codex/config.toml has hooks = false (or codex_hooks = false) under [features], remove it.

Restart your Codex sessions after the first install. Updating the safe-chains binary takes effect immediately.

Cursor CLI

Cursor exposes a dedicated beforeShellExecution event that fires only on shell calls, cleaner than a generic pre-tool hook. Run safe-chains --setup --tool=cursor to install. The config goes to ~/.cursor/hooks.json:

{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      {
        "command": "safe-chains hook cursor",
        "timeout": 30
      }
    ]
  }
}

Cursor hooks fail-open by default. If you want safe-chains failures to block (rather than silently letting commands through), add "failClosed": true to the entry.

Grok CLI (xAI)

Run safe-chains --setup --tool=grok to write ~/.grok/hooks/safe-chains.json. Grok’s shell tool is run_terminal_command, matched by Bash. Manual config:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "safe-chains hook grok",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

A Grok hook can only block a command — an allowing hook “declines to deny” rather than granting it, so safe-chains vetoes commands it doesn’t recognize and leaves safe ones to Grok’s own approval flow (same as Cursor and Codex).

One extra setup step. Grok auto-loads your ~/.claude/settings.json hooks for Claude compatibility. If safe-chains is also installed for Claude Code, Grok will additionally run the Claude hook on every command — it can’t read Grok’s request format, so it does nothing (harmless, but noisy). Silence it by adding to ~/.grok/config.toml:

[compat.claude]
hooks = false

Antigravity CLI (agy)

Run safe-chains --setup --tool=antigravity to write ~/.gemini/config/hooks.json. Antigravity matches its shell tool by the name run_command. Manual config:

{
  "PreToolUse": [
    {
      "matcher": "run_command",
      "hooks": [
        {
          "type": "command",
          "command": "safe-chains hook antigravity"
        }
      ]
    }
  ]
}

Note: Google sunset the Gemini CLI on June 18, 2026 and directs users to the Antigravity CLI (agy).

Qwen Code

Run safe-chains --setup --tool=qwen to write ~/.qwen/settings.json. Qwen mirrors Claude Code’s hook envelope verbatim. Manual config:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^Bash$",
        "hooks": [
          {
            "type": "command",
            "command": "safe-chains hook qwen",
            "timeout": 60000
          }
        ]
      }
    ]
  }
}

Factory Droid

Run safe-chains --setup --tool=droid to write ~/.factory/settings.json. Droid’s bash tool is named Execute (not Bash), and Droid requires absolute paths for hook commands, so the installer resolves the safe-chains binary’s absolute path at install time. Manual config (substitute the absolute path of your safe-chains binary):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Execute",
        "hooks": [
          {
            "type": "command",
            "command": "/usr/local/bin/safe-chains hook droid",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

GitHub Copilot CLI

Copilot’s hook config lives in ~/.copilot/hooks/*.json (user-global, files merge). Run safe-chains --setup --tool=copilot to write ~/.copilot/hooks/safe-chains.json. Copilot’s quirks: the response is a flat object (no hookSpecificOutput wrapper), the script-path field is bash (not command), and toolArgs is a JSON-encoded string on stdin (the safe-chains adapter double-decodes it). Manual config (substitute absolute path):

{
  "version": 1,
  "hooks": {
    "preToolUse": [
      {
        "type": "command",
        "bash": "/usr/local/bin/safe-chains hook copilot",
        "comment": "safe-chains: validate every Bash tool call before it runs.",
        "timeoutSec": 60
      }
    ]
  }
}

As of v1.0.71, Copilot honors both permissionDecision: "allow" and "deny".

OpenCode

We’d love to support OpenCode, but it currently has no way to run this kind of hook. You could hand-build a static permission.bash allowlist from safe-chains’ command list, but we don’t recommend it: a fixed list is brittle, inflexible, and by now impractically long. If OpenCode’s hook situation changes, please open an issue and we’ll take a look.