allow-rules
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.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!