Get into flow state. Permission hooks that reduce Claude Code prompt fatigue, with a CLI for managing composable community profiles.
Claude Code asks for permission on every tool call. If python is in your allow list but you run python3.13, you get prompted. If npm test is allowed but Claude runs npx jest, you get prompted. If Claude chains python3 a.py && echo done, you get prompted even though both are safe.
These hooks auto-approve safe, predictable operations so you can stay in flow.
- Bash alias resolution:
python3.13 script.pyauto-approved whenpythonis in your allow list - && and ; chain splitting:
python3 a.py && echo donesplits into sub-commands and resolves each independently - Multi-word command mapping:
npx jest --coverageauto-approved whennpm testis allowed - WebFetch domain matching: Documentation sites auto-approved (docs.rs, cppreference.com, etc.)
- File access control: Read/Edit/Write/Glob/Grep auto-approved for safe directories, with a hardcoded deny list for sensitive paths (~/.ssh, ~/.aws, .env files, etc.)
- Audit logging: Every decision logged to
~/.claude/hooks/hook.jsonl - Community profiles: Import and share curated permission rule sets
- Smart suggestions: Analyze your usage and recommend config changes
- Permission decay: Detect and remove stale rules to maintain least privilege
Option A: Test during development
# Launch Claude Code with the plugin loaded from a local directory
claude --plugin-dir /path/to/flow-state/plugins/flow-stateOption B: Permanent installation via marketplace
# Clone the repo
git clone https://github.com/asherliu/flow-state.git
cd flow-stateThen inside Claude Code:
/plugin marketplace add /path/to/flow-state
/plugin install flow-state@claude-code-hooks
The plugin's hooks are registered automatically via hooks.json -- no manual settings.json editing needed.
# Install the CLI (creates venv + installs the 'flowstate' command)
cd flow-state
uv sync
# Run setup (scans your allow list, generates permission-config.json)
uv run flowstate setupTo make flowstate available globally (outside the project venv):
uv tool install . # installs 'flowstate' into ~/.local/bin
flowstate setup # now works from anywhereSetup will:
- Check Python 3.10+ is available
- Scan your
~/.claude/settings.jsonallow list - Cross-reference against known alias families (python3.x, cargo-clippy, etc.)
- Generate
~/.claude/hooks/permission-config.jsonwith aliases, mappings, domains, file access paths, andresolvedCanonicals - Save your config to the lockfile for future mutations
Example output:
Scanning ~/.claude/settings.json...
Found 47 commands in your allow list
Generating alias rules...
12 alias rules from known variants
Generating domain rules...
8 documentation domains
Proceed? [Y/n] y
+ wrote ~/.claude/hooks/permission-config.json
+ saved user_config to lockfile
Done! Your next Claude Code session will have fewer permission prompts.
Run 'flowstate status' after a few sessions to see the impact.
Your next Claude Code session will have fewer permission prompts immediately. After a few sessions, run flowstate status or flowstate suggest to tune further.
Note: If you installed with
uv sync(notuv tool install), prefix commands withuv run:uv run flowstate status. Withuv tool install ., plainflowstateworks from anywhere.
flowstate setup # Interactive setup (balanced tier)
flowstate setup --tier cautious # Conservative: docs domains only, minimal file access
flowstate setup --tier balanced # Default: popular domains, common mappings, /tmp write
flowstate setup --tier flow # Permissive: broad domains, many mappings, ~/Documents write
flowstate setup --yes # Skip confirmation (scripting)
flowstate setup --dry-run # Preview without writing
flowstate setup --profile github:user/repo # Also install a profileflowstate statusShows: current tier, installed profiles, audit summary (allow/passthrough counts), top auto-approved rules, top passthrough candidates.
flowstate suggest # Show suggestions
flowstate suggest --apply # Apply suggestions interactively
flowstate suggest --min-count 5 # Only suggest groups with 5+ occurrencesAnalyzes your audit log to find patterns: versioned commands that should be aliased, domain groups that should be wildcarded. Suggestions are ranked by confidence (high/medium/low). Thresholds are tier-aware: cautious requires more evidence, flow suggests sooner.
flowstate tighten # Analyze and show stale rule candidates
flowstate tighten --apply # Remove/suppress stale rules interactively
flowstate tighten --days 60 # Custom staleness window (default: tier-dependent)
flowstate tighten --apply --force # Override the minimum data requirementThe complement to suggest. Analyzes your audit log to find rules that haven't triggered recently. User rules are removed directly; profile rules are suppressed via overrides.
flowstate clean # Analyze and apply consolidations interactively
flowstate clean --dry-run # Preview without applying
flowstate clean --yes # Apply without promptingScans your settings.json allow list for:
- Redundant Bash entries:
Bash(git log)whenBash(git *)already exists - Bash consolidations: 3+ specific entries like
Bash(git log),Bash(git status),Bash(git diff)→Bash(git *) - Redundant file path entries:
Read(src/main.py)whenRead(src/**)already exists (same-tool only) - File path consolidations: 2+ specific paths under the same directory →
Read(src/**) - Flow State handled (informational): entries also covered by alias rules
flowstate export --name my-rules --description "My Python dev rules" # stdout
flowstate export -o my-rules.json --name my-rules --description "My rules" # file
flowstate export --dry-run --name my-rules --description "Preview" # preview
flowstate export --include-write-paths --name my-rules --description "With writes"
flowstate export --include-risky --name my-rules --description "Keep internal domains"
flowstate export --exclude-pattern "*.internal.*" --name my-rules --description "Filtered"
flowstate export --source https://github.com/me/repo --tags "python,data-science" --name my-rules --description "With meta"Exports your local user rules as an installable profile JSON. By default:
- Only exports your own rules (installed profiles are not re-exported)
- Strips risky entries: internal domains (localhost, RFC1918, *.local, *.corp), absolute home paths
- Excludes writePaths (use
--include-write-pathsto include) - Adds
meta.recommendedTierfrom your current tier - Validates output against the profile schema
flowstate init github:user/rust-profile
flowstate init local:path/to/profile.jsonflowstate dry-run github:user/new-profileflowstate update # Update all profiles
flowstate remove rust-profilePlugin (single-process) CLI (management)
bash-gate.sh (thin dispatcher) flowstate setup / suggest / tighten
resolve.py (all Bash logic) writes to lockfile user_config
resolve_webfetch.py (WebFetch) generates permission-config.json
resolve_fileops.py (file ops)
| |
| reads ONE file | reads/writes
v v
permission-config.json profiles.lock.json
hook.jsonl (audit log) permission-config.json
Three hooks, each a single Python process. The shell dispatcher is a 5-line wrapper that forwards stdin to the Python entry point.
"python3.13 script.py && echo done"
|
[1] REJECT dangerous metacharacters (|, backticks, $(), >(), <()
|
[2] SPLIT on && and ; via shlex (respects quoting, cap 5 sub-commands)
-> ["python3.13 script.py", "echo done"]
|
[3] For each sub-command:
NORMALIZE binary (strip path, unwrap env/timeout)
-> DIRECT CANONICAL check (in resolvedCanonicals?)
-> ALIAS LOOKUP (python3.13 -> python, check resolvedCanonicals)
-> COMMAND MAPPING (longest prefix match)
|
[4] ALL sub-commands resolve -> {"decision": "allow"}
Any failure -> silent exit (defer to normal permissions)
WebFetch "https://docs.github.com/..."
|
Parse hostname -> "docs.github.com"
Check against webfetch.extraDomains
Match: exact or wildcard ("*.github.com")
|
Match -> {"decision": "allow"}
No match -> silent exit
Read "~/.claude/settings.json"
|
Extract path from tool_input (file_path or path, default: cwd)
Normalize: expand ~, resolve symlinks, make absolute
|
DENY check: hardcoded list (ssh, gnupg, aws, .env, etc.)
DENY check: config denyPaths
|
ALLOW check: readPaths (Read/Glob/Grep) or writePaths (Edit/Write)
|
Match -> {"decision": "allow"}
No match -> silent exit
Hardcoded deny list (cannot be overridden by config):
~/.ssh/**, ~/.gnupg/**, ~/.aws/**, ~/.netrc, ~/.claude/credentials*, ~/.password-store/**, ~/.local/share/keyrings/**, ~/.kube/**, ~/.docker/**, **/.env, **/.env.*, **/.git/config
Symlinks are resolved before matching -- a symlink pointing to ~/.ssh/id_rsa is still denied.
- Fail-closed: Hooks exit silently on any error. Only explicit matches produce auto-approval. You can never get less security than default Claude Code.
- No chaining: One level of alias indirection only.
mypy3 -> python3 -> pythonwill NOT chain. - resolvedCanonicals baked in: Hooks don't read settings.json at runtime. The CLI pre-computes which commands are canonical and writes them into permission-config.json.
Three tiers control what flowstate setup generates. Tiers are metadata — they set starting points, not enforcement limits.
| Cautious | Balanced (default) | Flow | |
|---|---|---|---|
| Domains | 8 docs sites | + 7 popular (GitHub, SO, npm) | + 3 more (Medium, dev.to, arxiv) |
| Read paths | ~/.claude/**, ~/.config/** |
+ /tmp/** |
+ ~/Documents/**, ~/Desktop/** |
| Write paths | none | /tmp/** |
+ ~/Documents/**, ~/Desktop/** |
| Command mappings | none | pip/pytest/npm | + cargo/docker |
| Suggest min_count | 5 | 3 | 2 |
| Tighten staleness | 14 days | 30 days | 60 days |
After setup, you can always add/remove rules via suggest, tighten, or profiles regardless of tier.
| File | Location | Purpose |
|---|---|---|
permission-config.json |
~/.claude/hooks/ |
Merged rules that hooks read (aliases, mappings, domains, fileAccess, resolvedCanonicals) |
profiles.lock.json |
~/.claude/hooks/ |
CLI's source of truth (user_config + profiles + overrides) |
hook.jsonl |
~/.claude/hooks/ |
Audit log (append-only JSONL) |
Profiles are JSON files that declare aliases, command mappings, and domain rules:
{
"name": "rust-dev",
"version": "1.0.0",
"description": "Rust development aliases and domains",
"bashAliases": {
"cargo-clippy": "cargo",
"cargo-fmt": "cargo"
},
"commandMappings": {
"cargo test": ["cargo nextest run"]
},
"webfetch": {
"extraDomains": ["*.docs.rs", "*.crates.io"]
}
}Profiles can also declare file access paths:
{
"fileAccess": {
"readPaths": ["~/.cargo/**", "~/.rustup/**"],
"writePaths": ["/opt/rust-cache/**"]
}
}Multiple profiles merge: domains are unioned, aliases are merged (user config takes priority on conflict), mappings are unioned per canonical, readPaths are unioned. Profile writePaths require explicit user opt-in during flowstate init. Profile caps: 50 aliases, 30 mappings, 50 domains, 50 file paths per sub-key.
Suppress individual profile rules without removing the entire profile:
flowstate tighten --apply # Creates overrides for stale profile rulesOverrides persist across flowstate update. When a profile author removes a rule you've already suppressed, the override is auto-cleaned (convergence detection).
Use flowstate suggest --apply to let the CLI recommend aliases based on your usage, or manually edit via the lockfile:
flowstate suggest --applySame approach -- suggest --apply will recommend domain wildcards, or you can run setup again.
The suggest command will recommend read paths based on your usage. You can also configure them through setup or profiles:
{
"fileAccess": {
"readPaths": ["~/.claude/**", "~/.config/**"],
"writePaths": ["/tmp/**"],
"denyPaths": ["~/.ssh/**", "~/.gnupg/**", "~/.aws/**"]
}
}- readPaths: Auto-approve Read, Glob, Grep to these paths
- writePaths: Auto-approve Edit, Write to these paths
- denyPaths: Always deny (checked before allow). The hardcoded deny list always applies regardless of config.
The key is the canonical command (must be in your allow list). The value is a list of equivalent commands:
{
"commandMappings": {
"npm test": ["npx jest", "yarn test", "pnpm test"],
"cargo test": ["cargo nextest run"]
}
}Setup automatically generates rules for these version families when the canonical command is in your allow list:
| Canonical | Auto-aliased variants |
|---|---|
| python | python3, python3.10-3.14 |
| pip | pip3 |
| pytest | py.test |
| cargo | cargo-clippy, cargo-fmt, cargo-watch |
| node | nodejs |
| npm | npx |
| ruby | ruby3.0-3.3 |
| gem | gem3 |
| java | java17, java21 |
| go | go1.21-1.23 |
- Claude Code CLI
- Python 3.10+
uv(for CLI commands)
MIT