A tool to check and enforce permissions for Claude Code.
- Bash syntax aware command analysis
- Glob based git path trust classification
- Tilde aware Read, Grep, and Glob path auto-allowing
- Implemented as a PreToolUse hook for Claude Code
- Reads JSON from stdin
- Evaluates all matching rules
- Returns the highest-priority decision: Deny > Ask > Allow
- Or, passes through to the default permission system.
Allowed and excluded paths are defined in settings.yaml using .gitignore style glob patterns.
Refer to the glob syntax guide.
Allow Read-Only Commands
Allow read-only commands:
base64basenamecatcolumncommandcutdirnameechofilefmtheadjqlesslsreadlinkrealpathrgstattailtrtreetypeuniqwcwhichxxd
Allow without in-place flags (-i, --in-place):
sort(without-o,--output)yq
Allow fd without exec flags (-x, --exec, -X, --exec-batch).
Allow read-only git subcommands:
git check-ignoregit describegit diffgit fetchgit loggit ls-treegit merge-basegit mvgit rev-parsegit rmgit showgit status
Allow bare or with read-only flags only (e.g. -a, --list, -v, --contains, --merged):
git branchgit taggit remote
Deny Unnecessary Destructive Commands
Deny all forms of rm. Suggests git rm -f or git clean -f <file> instead.
Deny find -delete and find -exec rm / find -execdir rm.
Deny fd -x rm / fd --exec rm / fd -X rm / fd --exec-batch rm.
Deny Destructive Git Operations
Deny destructive git operations:
git reset --hardgit stash popgit stash dropgit stash cleargit clean -d(any flag combo containing-d)git checkout --(discarding changes)
Trusted Git Paths
Handle git -C <path> by combining path trust classification with subcommand analysis.
- Destructive subcommands are denied regardless of path trust.
- Safe subcommands are allowed only in trusted paths.
- Deny
cd <path>chained withgitvia any operator (&&,||,;). - Suggests using
git -C <path>instead.
Chained push
Deny git push when part of a compound command. Requires it to be run standalone.
GitHub CLI
Allow read-only gh commands:
gh run listgh run viewgh release listgh api(without data flags or write methods)
Ask for approval on write operations:
gh pr commentgh apiwith data flags (-d,-f,-F,--input)gh apiwith write methods (-X POST/PUT/PATCH/DELETE)gh api graphqlwith mutations
Python
Deny inline Python (-c or heredoc) exceeding 1000 characters or 20 lines.
Insta review
Deny cargo insta review with heredoc input to prevent faking interactive input.
Bash rules classify command arguments using three approaches in increasing order of strictness. All three strip shell quotes and handle short-flag bundling (e.g. -an5).
Lightweight boolean matcher embedded directly in BashRule fields (with_any, with_all, without_any). Checks whether a flag or value is present in the argument list using glob patterns. No schema required.
Recognizes three flag-value forms: two-arg (-X POST), concatenated short (-XPOST), and equals long (--data=foo).
Best for simple presence/absence checks where you don't need structured parsing.
BashRule {
with_any: Some(vec![
ArgMatcher::new("-X").ivalue("{POST,PUT,PATCH,DELETE}"),
ArgMatcher::new("--data"),
]),
without_any: Some(vec![ArgMatcher::new("graphql")]),
..
}Walks a list of arguments against an ArgSchema that declares which flags are boolean and which take a value. Rejects unknown flags. Returns Vec<Arg> where each element is Flag, FlagPair, Operand, or Separator.
Best for rules that receive pre-sliced arguments and need to distinguish flags from positional values without subcommand awareness.
let settings = ArgParserSettings {
schema: ArgSchema {
bool_flags: vec![],
value_flags: vec![String::from("-b")],
},
unquote: true,
};
let parsed: Vec<Arg> = ArgParser::new(settings).parse(args)?;Walks the full token stream against a recursive CommandSchema tree. Each node defines its own options, operand constraints, and subcommands. Supports value validation via glob, regex, and exact-set constraints. Returns Vec<ParsedCommand> where index 0 is the root command, index 1 is the first subcommand, etc.
Best for rules that need the full command hierarchy, value validation, or operand count enforcement.
let schema = CommandSchemaBuilder::new("git")
.with_subcommand(
CommandSchemaBuilder::new("worktree")
.with_subcommand(
CommandSchemaBuilder::new("add")
.with_option(
OptionSchemaBuilder::new(["-b"])
.with_value(ValueConstraint::Any)
.build(),
)
.build(),
)
.build(),
)
.build();
let parsed: Vec<ParsedCommand> = CommandParser::new(schema).parse(args)?;source: utils/parsers/command/
brew install StudioLE/tap/hook-rscargo install --path .Enable the PreToolUse hooks in ~/.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "hook-rs bash" }]
},
{
"matcher": "Glob",
"hooks": [{ "type": "command", "command": "hook-rs glob" }]
},
{
"matcher": "Grep",
"hooks": [{ "type": "command", "command": "hook-rs grep" }]
},
{
"matcher": "Read",
"hooks": [{ "type": "command", "command": "hook-rs read" }]
}
]
}
}Refer to the glob syntax guide.
Settings are optional. If missing, defaults to empty.
Create a ~/.config/hook-rs/settings.yaml file with your settings:
git:
paths:
# Trust all repos in ~/repos
- ~/repos/**
# Exclude all repos in ~/repos/forked
#
- !~/repos/forked/**
# Trust all repos in ~/repos/forked/my-fork
- ~/repos/forked/my-fork/**
read:
paths:
# Allow reading the cargo registry
- ~/.cargo/registry/src/**
# Allow reading the rustup toolchain
- ~/.rustup/toolchains/**
# Allow reading any file in /path/to/repos
- /path/to/repos/**
# Allow reading any README.md
- README.md
# Exclude .env
- !.env
- !.env.*Note
While technically this is YAML tag syntax:
- !.envA pre-processor automatically converts it to a YAML string:
- "!.env"- Last match wins
!prefix excludes*matches zero or more characters except/?matches any single character except/**recursively matches directories{a,b}matchesaorbwhereaandbare arbitrary glob patterns (Nesting{...}is not currently allowed)[ab]matchesaorbwhereaandbare characters. Use [!ab] to match any character except for a and b.- Metacharacters such as
*and?can be escaped with character class notation. e.g.,[*]matches*.
Note
** recursively matches directories but are only legal in three situations:
- If the glob starts with
**/, then it matches all directories.
For example, **/foo matches foo and bar/foo but not foo/bar.
- If the glob ends with
/**, then it matches all sub-entries.
For example, foo/** matches foo/a and foo/a/b, but not foo.
- If the glob contains
/**/anywhere within the pattern, then it matches zero or more directories.
Using ** anywhere else is illegal
The glob ** is allowed and means "match everything".