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 /" — 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 as a PreToolUse hook. Or manually add to ~/.codex/hooks.json:

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

Codex requires [features] codex_hooks = true in ~/.codex/config.toml for hooks to fire. Add it manually if it isn’t already there:

[features]
codex_hooks = true

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.

Gemini CLI

Run safe-chains --setup --tool=gemini to write ~/.gemini/settings.json. Gemini’s hook event is BeforeTool (PascalCase) and the response key is decision (allow / deny — there’s no ask). Manual config:

{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": "^run_shell_command$",
        "hooks": [
          {
            "type": "command",
            "command": "safe-chains hook gemini",
            "timeout": 60000
          }
        ]
      }
    ]
  }
}

Note Gemini’s timeout is in milliseconds (other vendors use seconds).

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 — 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 .github/hooks/*.json (per-repo) or ~/.github/hooks/*.json (user-global, files merge). Run safe-chains --setup --tool=copilot to write ~/.github/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 late 2025 only permissionDecision: "deny" is honored by Copilot’s permission system; safe-chains emits "allow" envelopes anyway so future Copilot releases that honor the full schema get the upgrade for free.

OpenCode (experimental)

Generate OpenCode permission.bash rules from safe-chains’ command list:

safe-chains --opencode-config > opencode.json