name: allow-rules description: Manages cc-allow.toml configuration files for bash command permission control. Use when the user wants to add, modify, or remove allow/deny rules, redirect rules, or pipe rules for Claude Code bash commands. context: fork
Managing cc-allow Rules (v2 Config Format)
cc-allow evaluates bash commands and file tool requests (Read, Edit, Write) and returns exit codes: 0=allow, 1=ask (defer), 2=deny, 3=error.
Config Format Version
version = "2.0"
The v2 format is tool-centric with top-level sections: [bash], [read], [write], [edit].
Config Locations
-
~/.config/cc-allow.toml— Global defaults -
<project>/.claude/cc-allow.toml— Project-specific (searches up from cwd)
Merge behavior: All configs are evaluated and combined. deny > allow > ask. Within a config, most specific matching rule wins.
Config Structure
Bash Tool Configuration
[bash]
default = "ask" # "allow", "deny", or "ask"
dynamic_commands = "deny" # action for $VAR or $(cmd) as command name
default_message = "Command not allowed"
unresolved_commands = "ask" # "ask" or "deny" for commands not found
respect_file_rules = true # check file rules for command args
Shell Constructs
[bash.constructs]
function_definitions = "deny" # foo() { ... }
background = "deny" # command &
subshells = "ask" # (command)
heredocs = "allow" # <<EOF ... EOF (default: allow)
Aliases
Define reusable pattern aliases:
[aliases]
project = "path:$PROJECT_ROOT/**"
plugin = "path:$CLAUDE_PLUGIN_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]]
# base allow (lower specificity)
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]]
# base allow for git push
[[bash.allow.docker.compose.up]]
# matches: docker compose up
This is equivalent to args.position:
-
[[bash.deny.git.push]]= commandgitwithposition.0 = "push" -
[[bash.allow.docker.compose.up]]= commanddockerwithposition.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"] # at least one must match (OR)
args.all = ["path:*.txt"] # all args must match (AND)
args.not = { any = ["--dry-run"] } # negate the result
args.position = { "0" = "/etc/*" } # absolute positional match
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"] }
]
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"] # pipes directly to one of these
pipe.from = ["curl", "wget"] # receives from any upstream
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 # only match >> (omit for both > and >>)
paths = [".bashrc", ".zshrc"]
Heredocs
# Deny all heredocs
[bash.constructs]
heredocs = "deny"
# Or use fine-grained rules (only checked if constructs.heredocs = "allow")
[[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/**"] # NOT under /etc
args.any = ["!path:*.txt"] # NOT .txt files
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 |
$CLAUDE_PLUGIN_ROOT |
Plugin root 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)
ref: Cross-References
Use ref: to reference other config values:
# Reference file rule paths for cp/mv
[[bash.allow.cp]]
args.position = { "0" = "ref:read.allow.paths", "1" = "ref:write.allow.paths" }
# Reference an alias
[[bash.allow.rm]]
args.any = ["ref:aliases.project"]
Resolution:
-
ref:read.allow.paths→ resolves to[read.allow].paths -
ref:aliases.project→ resolves to the alias value
Per-Rule File Configuration
[[bash.allow.tar]]
respect_file_rules = false # disable file checking for complex args
[[bash.allow.mycommand]]
file_access_type = "Write" # force specific 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
Workflow
- If no project config exists, initialize one:
${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --init - Read the existing config at
.claude/cc-allow.toml - Determine what change is needed
- Add new rules
- Write the updated config
- Validate with
--fmtto check syntax and view rules by specificity:${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --fmt - Test the new rule with a matching command:
# Test bash command echo 'git push --force' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow echo $? # 0=allow, 1=ask, 2=deny # Test file tools echo '/etc/passwd' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --read echo '$HOME/.bashrc' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --write - Use
--debugfor detailed evaluation trace:echo 'git push --force' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --debug
chat Comments (0)
Sign in to join the discussion and leave a comment.
Skill Details
Related Skills
Build your own?
Join 12,000+ developers contributing to the Claude ecosystem.
No comments yet. Be the first to share your thoughts!