| name | allow-rules |
| description | Manages cc-allow.toml configuration files for bash command, file tool, search tool, and WebFetch URL permission control. Use when the user wants to add, modify, or remove allow/deny rules, redirect rules, pipe rules, or URL rules for Claude Code tools. |
| context | fork |
Managing cc-allow Rules (v2 Config Format)
cc-allow evaluates bash commands, file tool requests (Read, Edit, Write), search tool requests (Glob, Grep), and WebFetch URL requests and returns exit codes: 0=allow, 1=ask (defer), 2=deny, 3=error.
The current session id is ${CLAUDE_SESSION_ID}
Config Format Version
version = "2.0"
The v2 format is tool-centric with top-level sections: [bash], [read], [write], [edit], [glob], [grep], [webfetch].
Config Locations
~/.config/cc-allow.toml -- Global/user defaults (loosest)
<project>/.config/cc-allow.toml -- Project-specific (searches up from cwd)
<project>/.config/cc-allow.local.toml -- Local overrides (gitignored)
<project>/.config/cc-allow/sessions/<session-id>.toml -- Session-scoped (auto-cleaned)
<project>/.config/cc-allow/<agent>.toml -- Agent-specific configs (used with --agent)
Merge behavior: All configs merged. deny > allow > ask. Most specific matching rule wins.
Config Structure
Bash Tool Configuration
[bash]
default = "ask"
dynamic_commands = "deny"
default_message = "Command not allowed"
unresolved_commands = "ask"
respect_file_rules = true
Shell Constructs
[bash.constructs]
function_definitions = "deny"
background = "deny"
subshells = "ask"
heredocs = "allow"
Command Classification
Classify commands for file rule checking (orthogonal to allow/deny):
[bash.read]
commands = ["cat", "less", "grep", "head", "tail", "find"]
[bash.write]
commands = ["rm", "mkdir", "chmod", "touch"]
[bash.edit]
commands = ["sed", "awk"]
When respect_file_rules = true, classified commands have their file arguments checked against the corresponding [read]/[write]/[edit] rules.
Built-in defaults (used when no classification sections exist in any config):
- Read:
cat, less, more, head, tail, grep, egrep, fgrep, rg, find, file, readlink, wc, diff, cmp, comm, stat, md5sum, sha256sum, sha1sum, od, xxd, hexdump, strings, sort, uniq, cut, tr, awk, sed, jq, yq, tee, xargs
- Write:
rm, rmdir, touch, mkdir, mktemp, chmod, chown, chgrp, unlink
- Positional (source=Read, dest=Write):
cp, mv, ln, install, rsync, scp
Once any config defines a classification section, built-in defaults are replaced. Later configs override per-command. A command in multiple sections within one file is an error.
Aliases
Define reusable pattern aliases:
[aliases]
project = "path:$PROJECT_ROOT/**"
safe-write = ["path:$PROJECT_ROOT/**", "path:/tmp/**"]
sensitive = ["path:$HOME/.ssh/**", "path:**/*.key", "path:**/*.pem"]
Reference with alias: prefix (aliases cannot reference other aliases):
[read.allow]
paths = ["alias:project", "alias:plugin"]
[read.deny]
paths = ["alias:sensitive"]
[[bash.allow.rm]]
args.any = ["alias:project"]
Allow/Deny Command Lists
[bash.allow]
commands = ["ls", "cat", "git", "go"]
[bash.deny]
commands = ["sudo", "rm", "dd"]
message = "{{.Command}} blocked - dangerous command"
Complex Rules with Argument Matching
For fine-grained control, use [[bash.allow.X]] or [[bash.deny.X]]:
[[bash.deny.rm]]
message = "{{.ArgsStr}} - recursive deletion not allowed"
args.any = ["flags:r", "--recursive"]
[[bash.allow.rm]]
Subcommand Nesting
[[bash.allow.git.status]]
[[bash.allow.git.diff]]
[[bash.deny.git.push]]
message = "{{.ArgsStr}} - force push not allowed"
args.any = ["--force", "flags:f"]
[[bash.allow.git.push]]
[[bash.allow.docker.compose.up]]
This is equivalent to args.position:
[[bash.deny.git.push]] = command git with position.0 = "push"
[[bash.allow.docker.compose.up]] = command docker with position.0 = "compose", position.1 = "up"
Specificity with nesting: +50 per nesting level
[[bash.allow.git]] → 100
[[bash.allow.git.push]] → 150
[[bash.allow.docker.compose.up]] → 200
Rule Specificity
When multiple rules match, most specific rule wins. Rule order doesn't matter.
Specificity points: Named command (+100), each subcommand (+50), each position arg (+20), each pattern in args.any/all (+5), each pipe target (+10), pipe from wildcard (+5). Tie-break: deny > ask > allow.
Argument Matching
Boolean expression operators:
args.any = ["-r", "-rf"]
args.all = ["path:*.txt"]
args.not = { any = ["--dry-run"] }
args.position = { "0" = "/etc/*" }
Position with Enum Values
Position values can be arrays (OR semantics):
[[bash.allow.git]]
args.position = { "0" = ["status", "diff", "log", "branch"] }
[[bash.deny.git]]
args.position = { "0" = ["push", "pull", "fetch", "clone"] }
Relative Position Sequences
args.any and args.all support sequence objects for adjacent arg matching:
[[bash.allow.ffmpeg]]
args.any = [
{ "0" = "-i", "1" = "path:$HOME/**" },
"re:^--help$"
]
[[bash.allow.openssl]]
args.all = [
{ "0" = "-in", "1" = ["path:*.pem", "path:*.crt"] },
{ "0" = "-out", "1" = ["path:*.pem", "path:*.der"] }
]
Per-Position IO Types
Use "N.type" keys to specify file access type per position (in both args.position and sequence objects):
[[bash.allow.cp]]
args.position = { "0.read" = "path:**", "1.write" = "path:**" }
[[bash.allow.ffmpeg]]
args.any = [
{ "0" = "-i", "1.read" = "path:$PROJECT_ROOT/**" },
{ "0" = "-o", "1.write" = "path:$PROJECT_ROOT/**" },
]
The .type suffix (read, write, edit, pattern, skip) overrides the command's classification for that argument. Use pattern or skip to mark positions as non-file (e.g., search patterns, expressions).
Key distinction:
args.position = absolute positions (arg[0] must be X)
- Objects in
args.any/args.all = relative positions (sliding window)
Pipe Context
pipe.to = ["bash", "sh"]
pipe.from = ["curl", "wget"]
Use from = ["path:*"] to match any piped input.
Redirects
[bash.redirects]
respect_file_rules = true
[[bash.redirects.allow]]
paths = ["/dev/null"]
[[bash.redirects.deny]]
message = "Cannot write to system paths"
paths = ["path:/etc/**", "path:/usr/**"]
[[bash.redirects.deny]]
message = "Cannot append to shell config"
append = true
paths = [".bashrc", ".zshrc"]
Heredocs
[bash.constructs]
heredocs = "deny"
[[bash.heredocs.deny]]
message = "Dangerous content"
content.any = ["re:DROP TABLE", "re:DELETE FROM"]
Pattern Matching
| Prefix | Description | Example |
|---|
path: | Glob pattern with variable expansion | path:*.txt, path:$PROJECT_ROOT/** |
re: | Regular expression | re:^/etc/.* |
flags: | Flag pattern (chars must appear) | flags:rf, flags[--]:rec |
alias: | Reference to path alias | alias:project, alias:sensitive |
ref: | Config cross-reference | ref:read.allow.paths |
| (none) | Exact literal match | --verbose |
Negation
Prepend "!" to patterns with explicit prefixes:
args.any = ["!path:/etc/**"]
args.any = ["!path:*.txt"]
Note: Negation requires an explicit prefix. !foo matches the literal string "!foo".
Path Variables
| Variable | Description |
|---|
$PROJECT_ROOT | Directory containing .claude/ or .git/ |
$HOME | User's home directory |
File Tool Permissions
Separate top-level sections for each file tool:
[read]
default = "ask"
[read.allow]
paths = ["alias:project", "alias:plugin"]
[read.deny]
paths = ["alias:sensitive"]
message = "Cannot read sensitive files"
[edit]
default = "ask"
[edit.allow]
paths = ["alias:project"]
[edit.deny]
paths = ["path:$HOME/.*"]
[write]
default = "ask"
[write.allow]
paths = ["alias:project", "path:/tmp/**"]
[write.deny]
paths = ["path:$HOME/.*", "path:/etc/**", "path:/usr/**"]
message = "Cannot write outside project"
Evaluation order: deny → allow → default (deny always wins)
Search Tool Permissions (Glob/Grep)
Glob and Grep only have a path parameter — no command to evaluate. By default they delegate to Read rules via respect_file_rules:
[glob]
respect_file_rules = true
[grep]
respect_file_rules = true
When respect_file_rules = true (default), the search path is checked against [read] rules. The default action is "allow", so Read rules are the sole authority.
Optional tool-specific deny rules:
[glob.deny]
paths = ["path:/var/log/**"]
message = "Cannot search {{.FilePath}}"
Set respect_file_rules = false to ignore Read rules and use only tool-specific rules.
CLI testing:
echo '/etc' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --glob
echo '/home/user/project' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --grep
WebFetch Tool Permissions
Control Claude Code's WebFetch tool with URL pattern matching and optional Google Safe Browsing:
[webfetch]
default = "ask"
default_message = "URL fetch requires approval: {{.FilePath}}"
[webfetch.allow]
paths = [
"re:^https://github\\.com/",
"re:^https://api\\.github\\.com/",
"re:^https://pkg\\.go\\.dev/",
"re:^https://docs\\.",
]
[webfetch.deny]
paths = [
"re:^https?://localhost",
"re:^https?://127\\.0\\.0\\.1",
"re:^file://",
]
message = "Blocked URL: {{.FilePath}}"
Important: URL patterns must use re: prefix. The path: prefix is for filesystem paths and won't work for URLs.
Evaluation order: deny → allow → Safe Browsing (if enabled) → default
Google Safe Browsing
Enable automatic URL threat detection:
[webfetch.safe_browsing]
enabled = true
api_key = "AIza..."
When enabled, URLs not matching any local pattern are checked against Google Safe Browsing v4 API. Flagged URLs are denied. On API errors, fails open with 'ask' (defers to claude).
Merge: strictest-wins — once enabled by any config, cannot be disabled. API key uses last-config-wins.
ref: Cross-References
Use ref: to reference other config values:
[[bash.allow.cp]]
args.position = { "0" = "ref:read.allow.paths", "1" = "ref:write.allow.paths" }
[[bash.allow.rm]]
args.any = ["ref:aliases.project"]
Resolution:
ref:read.allow.paths → resolves to [read.allow].paths
ref:glob.deny.paths → resolves to [glob.deny].paths
ref:aliases.project → resolves to the alias value
Per-Rule File Configuration
[[bash.allow.tar]]
respect_file_rules = false
[[bash.allow.mycommand]]
file_access_type = "Write"
[[bash.ask.sed]]
args.any = ["flags:i"]
file_access_type = "Edit"
file_access_type overrides the command's bulk classification from [bash.read/write/edit]. Per-position IO types ("N.type") override file_access_type.
Message Templates
[[bash.deny.rm]]
message = "{{.ArgsStr}} - recursive deletion not allowed"
[write.deny]
message = "Cannot write to {{.FilePath}} - system directory"
| Field | Description | Available For |
|---|
{{.Command}} | Command name | Command rules |
{{.ArgsStr}} | Arguments as string | Command rules |
{{.Arg 0}} | First argument | Command rules |
{{.PipesFrom}} | Upstream commands | Command rules |
{{.Target}} | Redirect target | Redirect rules |
{{.FilePath}} | File path | File rules |
{{.FileName}} | File base name | File rules |
{{.Tool}} | File tool name | File rules |
Common Tasks
Allow a command: Add to [bash.allow].commands or create [[bash.allow.X]]
Block a command: Add to [bash.deny].commands or create [[bash.deny.X]]
Block with specific args: Use [[bash.deny.X]] with args.any or args.all
Block subcommand: Use nested path like [[bash.deny.git.push]]
Restrict to project: Use alias:project or path:$PROJECT_ROOT/**
Block piping to shell: Use [[bash.deny.bash]] with pipe.from = ["curl", "wget"]
Allow file reading: Add to [read.allow].paths
Block file writing: Add to [write.deny].paths
Allow URL fetching: Add re: pattern to [webfetch.allow].paths
Block URL fetching: Add re: pattern to [webfetch.deny].paths
Enable Safe Browsing: Set [webfetch.safe_browsing] enabled = true with api_key
Control search tools: Set [glob]/[grep] with respect_file_rules = true (default) to inherit Read rules
Classify a command for file rules: Add to [bash.read], [bash.write], or [bash.edit] commands list
Context-sensitive classification: Use file_access_type on a [[bash.X.command]] rule with arg matching (e.g., sed -i as Edit)
Mark args as non-file: Use "N.pattern" or "N.skip" IO type in args.position or sequence objects to exclude arguments from file rule checking
Note on pattern-first commands: grep, sed, awk, jq, yq, and rg automatically skip their first non-flag argument (the pattern/expression) during file rule checking. Flags like -e/--regexp (grep) and -e/--expression (sed) also skip their consumed argument. No configuration needed for these built-in behaviors.
Scope Detection
When the user asks to add or modify rules, determine the appropriate config scope from their phrasing:
Session (default when no scope mentioned):
- The current session id is ${CLAUDE_SESSION_ID}
- Explicit: "session", "this session", "for now", "just for now", "temporarily"
- Implicit: no scope keyword at all -- default to session
- File:
<project>/.config/cc-allow/sessions/<session-id>.toml
Project:
- Keywords: "project", "this project", "permanently", "always"
- File:
<project>/.config/cc-allow.toml
Global/User:
- Keywords: "global", "user", "all projects", "everywhere", "globally"
- File:
~/.config/cc-allow.toml
When creating a new session config, initialize with version = "2.0" and create the sessions directory if needed:
mkdir -p <project>/.config/cc-allow/sessions
Settings
[settings]
session_max_age = "7d"
Workflow
- If no project config exists, initialize one:
${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --init
- Determine scope (session/project/global) from the user's request
- Read the appropriate config file for that scope
- Add new rules
- Write the updated config
- Validate with
--fmt to check syntax and view rules by specificity:
${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --fmt
- Test the new rule with a matching command:
echo 'git push --force' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow
echo $?
echo '/etc/passwd' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --read
echo '$HOME/.bashrc' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --write
echo '/etc' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --glob
echo '/home/user/project' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --grep
echo 'https://github.com/user/repo' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --fetch
echo 'npm install' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --agent playwright
- Use
--debug for detailed evaluation trace:
echo 'git push --force' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --debug
Agent-Specific Configs
Create configs for specific subagent types in .config/cc-allow/<agent>.toml:
mkdir -p .config/cc-allow
cat > .config/cc-allow/playwright.toml << 'EOF'
version = "2.1"
[bash]
default = "deny"
[bash.allow]
mode = "replace"
commands = ["npx", "node"]
[[bash.allow.npx.playwright]]
EOF
Use with --agent:
echo 'npx playwright test' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --agent playwright