From cc-allow
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.
npx claudepluginhub dannycoates/cc-allow --plugin cc-allowThis skill uses the workspace's default tool permissions.
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.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
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}
version = "2.0"
The v2 format is tool-centric with top-level sections: [bash], [read], [write], [edit], [glob], [grep], [webfetch].
~/.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.
[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
[bash.constructs]
function_definitions = "deny" # foo() { ... }
background = "deny" # command &
subshells = "ask" # (command)
heredocs = "allow" # <<EOF ... EOF (default: allow)
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):
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, xargsrm, rmdir, touch, mkdir, mktemp, chmod, chown, chgrp, unlinkcp, mv, ln, install, rsync, scpOnce 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.
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"]
[bash.allow]
commands = ["ls", "cat", "git", "go"]
[bash.deny]
commands = ["sudo", "rm", "dd"]
message = "{{.Command}} blocked - dangerous command"
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)
[[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]] = 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]] → 200When 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.
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 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"] }
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"] }
]
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)args.any/args.all = relative positions (sliding window)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.
[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"]
# 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"]
| 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 |
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".
| Variable | Description |
|---|---|
$PROJECT_ROOT | Directory containing .claude/ or .git/ |
$HOME | User's home directory |
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)
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
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
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.
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].pathsref:glob.deny.paths → resolves to [glob.deny].pathsref:aliases.project → resolves to the alias value[[bash.allow.tar]]
respect_file_rules = false # disable file checking for complex args
[[bash.allow.mycommand]]
file_access_type = "Write" # force specific access type
# Override classification for specific arg patterns
[[bash.ask.sed]]
args.any = ["flags:i"]
file_access_type = "Edit" # sed -i edits files (overrides bulk classification)
file_access_type overrides the command's bulk classification from [bash.read/write/edit]. Per-position IO types ("N.type") override file_access_type.
[[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 |
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.
When the user asks to add or modify rules, determine the appropriate config scope from their phrasing:
Session (default when no scope mentioned):
<project>/.config/cc-allow/sessions/<session-id>.tomlProject:
<project>/.config/cc-allow.tomlGlobal/User:
~/.config/cc-allow.tomlWhen 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]
session_max_age = "7d" # auto-delete session configs older than this
${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --init
--fmt to check syntax and view rules by specificity:
${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --fmt
# 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
# Test search tools
echo '/etc' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --glob
echo '/home/user/project' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --grep
# Test WebFetch URLs
echo 'https://github.com/user/repo' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --fetch
# Test with agent-specific config
echo 'npm install' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --agent playwright
--debug for detailed evaluation trace:
echo 'git push --force' | ${CLAUDE_PLUGIN_ROOT}/bin/cc-allow --debug
Create configs for specific subagent types in .config/cc-allow/<agent>.toml:
# Create agent config directory
mkdir -p .config/cc-allow
# Create playwright-specific rules
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