iTranslated by AI
Standardizing CLI commands makes managing Claude Code permissions easier
When you start configuring permissions in Claude Code, you gradually run into some difficulties.
What I got stuck on wasn't the syntax of the permissions themselves, but rather the inconsistency in how commands are written in the CLI.
For example, specifying a profile is crucial for aws, and the same gh api command can change its meaning from read to write depending on the options used. If you try to handle all of this purely through permissions, the configuration becomes increasingly difficult to manage.
So far, I have found that it works better to decide on the standard forms of the commands Claude is allowed to use first.
What I do is simple:
- Teach Claude how to use commands in
CLAUDE.mdbeforehand - Apply allow / ask / deny settings in
settings.jsonbased on those forms - Use
hooksto stop any deviations
The structure looks like this:
~/.claude/
├── CLAUDE.md
├── settings.json
└── hooks/
└── bash-guard.sh
First, define the canonical form of commands in CLAUDE.md
I start by writing how Claude should call Bash in CLAUDE.md.
# Shell commands
Always follow these rules when calling Bash:
- `aws` must always be executed in the form `aws --profile <profile> ...`
- Read operations for `gh api` must always be executed in the form `gh api --method GET ...`
- Do not use `gh api` to execute write operations using `-f`, `-F`, `--input`, `-X`, or `--method`
- Do not use path specification options like `git -C`. Execute `cd` first, then run the command
I believe it is quite important not to rely solely on hooks for this.
Of course, if you block them with a hook, Claude will see the error message and autonomously correct how it calls the command. That is certainly useful in its own way.
However, there is a slight lag every time that happens. It only takes a few seconds, but it adds up, which I find a bit annoying.
Therefore, it is smoother to have Claude know the usage from the start, even if the hook could technically correct it. I write it in CLAUDE.md for that reason.
Write permissions based on those canonical forms
Once you fix the command forms, writing permissions becomes much easier.
For example, if you want to separate aws by profile, it looks like this:
{
"permissions": {
"allow": [
"Bash(aws --profile readonly-*)"
],
"ask": [
"Bash(aws --profile write-*)"
],
"deny": [
"Bash(git push --force *)",
"Bash(git reset --hard *)"
]
}
}
Specific naming depends on your environment, but in terms of the approach, I found it easiest to classify them as:
- Allow for read operations
- Ask for updates
- Deny for destructive operations
Use hooks as a last line of defense
On top of that, use hooks to block only those that deviate from the canonical form.
The PreToolUse configuration looks like this:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/bash-guard.sh"
}
]
}
]
}
}
Example of bash-guard.sh
The content is quite simple.
#!/bin/bash
set -eu
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
[ -z "$cmd" ] && exit 0
# Prohibit git -C
if echo "$cmd" | grep -qE '^\s*git\s+-C\b'; then
echo "BLOCKED: Do not use git -C; perform a cd first, then run the command." >&2
exit 2
fi
# --profile is mandatory for aws
if echo "$cmd" | grep -qE '^\s*aws\s' && ! echo "$cmd" | grep -q -- '--profile'; then
echo "BLOCKED: Please include --profile with aws." >&2
exit 2
fi
# Only permit read-only forms for gh api
if echo "$cmd" | grep -qE '^\s*gh\s+api\b'; then
if ! echo "$cmd" | grep -q -- '--method GET'; then
echo "BLOCKED: Please execute gh api read operations in the form: gh api --method GET ..." >&2
exit 2
fi
if echo "$cmd" | grep -qE '(^|[[:space:]])(-f|--raw-field|-F|--field|--input)([[:space:]]|=)'; then
echo "BLOCKED: Please do not use -f / -F / --input with gh api. Only read operations are permitted." >&2
exit 2
fi
fi
exit 0
The benefit of hooks is that Claude can fix it itself
What I like about this configuration is that the hook is not just a blocker.
For example, by returning messages like:
Please include --profile with awsDo not use git -C; perform a cd firstPlease execute gh api in the form: gh api --method GET ...
Claude sees that message and corrects its next execution.
In other words, the hook is quite useful as a mechanism to re-teach usage to Claude on the fly rather than just a simple prohibition mechanism.
That said, as mentioned earlier, there is a small lag because it stops the action once and then performs the correction. Therefore, I have settled on the approach of writing regular usage in CLAUDE.md and using hooks as a final safety net.
Division of roles
For now, this is how I organize them:
-
CLAUDE.md- Write the rules you want Claude to follow from the beginning
-
permissions- Apply allow / ask / deny settings to those canonical forms
-
hooks- Stop deviations and return the next correction method to Claude
Since adopting this division, designing permissions has become much easier.
Conclusion
When working with Claude Code permissions, you start to worry more about the inconsistency in how the CLI is written than the permission syntax itself.
I found that it worked better to decide on the canonical forms of the commands Claude uses first, rather than trying to absorb everything through permissions alone.
What I do is simple:
- Write usage rules in
CLAUDE.md - Apply permissions in
settings.json - Stop deviations using the
PreToolUsehook - Return the next instruction method to Claude via error messages
For now, this is the easiest way to handle it.
Discussion