Supercharging Claude Code Permissions with Dippy
Claude Code asks for permission before running shell commands and MCP tools. Out of the box, you manage this through settings.json, a flat JSON list of allow and deny entries. My main frustration was wildcards: I wanted to write Bash(kubectl -n * get) to match any namespace, but at the time I thought settings.json only supported prefix matching. No way to put a * in the middle of a command.
That’s what led me to Dippy. And while writing this post, I discovered that Claude Code actually added wildcard support at any position back in v2.1.0 (January 2026). So Bash(kubectl -n * get) does work natively now. Good to know.
Still, Dippy does things Claude Code can’t do natively, so I’m sticking with it.
What is Dippy

🐤 Dippy is a CLI tool that runs as a Claude Code hook. It intercepts PreToolUse events (before Claude executes a Bash command or MCP tool) and decides: allow, ask (prompt with a message), or deny.
The config is a plain text file with pattern matching:
allow git status # auto-approve
ask git push "Confirm push target" # prompt with message
deny rm -rf "Use trash instead" # block with explanationWhat makes it better than settings.json:
- Guidance messages - settings.json has allow, ask, and deny, but the prompts are bare. Dippy lets you attach a message:
deny helm install "Use GitOps: create a HelmRelease in the k8s/flux repo instead"teaches Claude the right approach without me explaining it every time - Last match wins - rules are ordered and the last match takes precedence, in any direction. In settings.json, deny always wins over allow regardless of order, so you can’t do “deny git push, but allow git push origin main”. Dippy’s ordering gives you full control
- Plain text config - much easier to organize, read, and maintain than a JSON array. Comments, categories, blank lines for grouping
- File redirect controls -
deny-redirect **/.env* "Never write secrets"blocks output redirection to sensitive files, even if the command itself is allowed
The Config
Dippy’s config reads like documentation. Here’s how I organized mine:
Default Behavior
set default askUnknown commands prompt for approval rather than silently blocking. This is important: I want to know when Claude tries something new, not have it fail silently.
File Redirect Controls
deny-redirect **/.env* "Never write secrets; do it manually"
deny-redirect **/*credentials* "Never write credentials; do it manually"
deny-redirect **/*.pem "Never write key files; do it manually"
deny-redirect **/id_rsa* "Never write SSH keys; do it manually"This catches output redirection. Even if a command is allowed, writing its output to a secrets file gets blocked with a clear message.
Read vs Write Separation
The biggest improvement over settings.json is separating read-only operations from write operations. Git is a good example:
# Git read operations - auto-approve
allow git status
allow git log
allow git diff
allow git show
allow git fetch
allow git branch
allow git blame
# Git write operations - prompt first, except pull
allow git pull
ask git push "Confirm push target"
ask git commit "Confirm commit"
ask git add "Confirm staging"
# Destructive git operations - prompt with warning
ask git push --force "WARNING: force-push can destroy remote history"
ask git reset --hard "WARNING: discards uncommitted changes permanently"
ask git branch -D "WARNING: force-deletes branch; consider git branch -d"“Last match wins” makes this work. allow git would match git push, but the more specific ask git push rule comes later and takes precedence.
I use ask instead of deny for destructive operations. The warning message shows me why I should be careful, but I can still approve it when I actually want it. With deny, Claude would just give up and suggest I do it manually.
Kubernetes, Helm, Flux
Same pattern: read operations are auto-approved, write operations prompt, and some are denied with guidance:
# Read - auto-approve
allow kubectl get
allow kubectl describe
allow kubectl logs
allow helm list
allow flux get
# Write - prompt
ask kubectl apply "Confirm resource apply"
ask kubectl delete "WARNING: will delete cluster resources"
deny helm install "Use GitOps: create a HelmRelease in the k8s/flux repo instead"
deny helm uninstall "Use GitOps: remove the HelmRelease from the k8s/flux repo instead"GitHub CLI
The same read/write pattern works for API tools. Viewing and listing are safe, but creating issues, closing them, or posting comments are visible to others and shouldn’t happen silently:
# Read - auto-approve
allow gh issue view
allow gh issue list
allow gh pr view
allow gh search
# Write - prompt (creates visible artifacts)
ask gh issue create "Confirm issue creation"
ask gh issue close "Confirm issue close"
ask gh issue comment "Confirm posting comment"
ask gh pr create "Confirm PR creation"
ask gh pr comment "Confirm posting PR comment"gh api gets the same curl-style treatment: GET requests are allowed, but write methods prompt:
allow gh api
ask gh api * -X POST "Review POST request before sending"
ask gh api * -X DELETE "Review DELETE request before sending"Unlisted gh commands (like gh repo delete or gh pr merge) fall through to set default ask, so they still prompt. No broad allow gh needed.
Network Requests
Curl is interesting because curl -s (read) and curl -X POST (write) are very different operations:
allow curl
# Write operations - catch flags regardless of position (last match wins)
ask curl * -X POST "Review POST request before sending"
ask curl * -X PUT "Review PUT request before sending"
ask curl * -X DELETE "Review DELETE request before sending"
ask curl * -X PATCH "Review PATCH request before sending"
ask curl * -d "Review request with data before sending"
ask curl * -F "Review form upload before sending"
ask curl * -T "Review file upload before sending"MCP Tools
Dippy also handles MCP tool permissions. I added a second hook matcher for mcp__.* and moved all MCP rules out of settings.json:
# Documentation tools
allow-mcp mcp__context7__resolve-library-id
allow-mcp mcp__context7__query-docs
# Monitoring (read operations + ask for writes)
allow-mcp mcp__prometheus__*
allow-mcp mcp__grafana__*
ask-mcp mcp__grafana__update_dashboard "Confirm dashboard update"
# Kubernetes MCP
allow-mcp mcp__kubernetes__resources_get
allow-mcp mcp__kubernetes__resources_list
allow-mcp mcp__kubernetes__pods_listPer-Project Overrides
The global config covers common tools. Project-specific rules go in .dippy files at the repo root. Dippy’s precedence: project .dippy overrides global ~/.dippy/config.
I created 8 project .dippy files for repos that need extra permissions. For example, my Home Assistant repo needs access to the Home Assistant MCP server, which doesn’t make sense globally:
# hasspi/.dippy
allow-mcp mcp__hass-mcp__*Or my Flux monorepo, which needs a project-specific Harbor API script and the Flux operator MCP:
# k8s/flux/.dippy
allow /path/to/harbor-api.py exec
allow-mcp mcp__flux-operator-mcp__*The Migration from settings.json to Dippy
I pointed Claude at my global settings.json (191 entries) and 14 project-local settings.local.json files, and asked it to explore the full permission landscape, categorize everything, and migrate it all to Dippy configs.
The process went roughly like this:
- Audit - Claude read every settings file, categorized all 191 entries by tool type (git, kubectl, helm, docker, etc.), and identified duplicates across repos
- Create global Dippy config - Translated all Bash and MCP permissions into Dippy format, organized by category, with read/write separation
- Slim down settings.json - Removed all
Bash(...)andmcp__*entries, keeping only Read, WebFetch, and Skill permissions (191 → 4 entries) - Migrate project-local permissions - For each repo, decided what to promote to global vs keep project-specific. Created 8 project
.dippyfiles - Add MCP hook - Added a second PreToolUse matcher for
mcp__.*so Dippy intercepts MCP tools too - Validate - Tested that all previously allowed commands still work, no new prompts for safe operations
- Create a skill - Asked Claude to build a skill for managing Dippy permissions going forward. Now when I need to allow a new command, I just say
/claude-permissions allow devbox run -- hugo server -Dand Claude handles the config edit. I review the diff, not write it
Quick Start
Install Dippy:
brew install ldayton/tap/dippyCreate a config at ~/.dippy/config:
set default ask
allow git status
allow git log
allow git diff
allow kubectl get
allow kubectl describe
ask git push "Confirm push target"
deny rm -rf "Use trash instead"Add the hook to your Claude Code settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "dippy" }
]
}
]
}
}That’s it. Every Bash command Claude runs now goes through Dippy first.
Results
The day-to-day workflow is noticeably better:
- No more permission fatigue - safe commands auto-approve, dangerous ones prompt with context
- Self-documenting - the config file has comments and categories;
settings.jsonwas just a wall of strings - Guidance over blocking - when Claude tries
helm install, it sees “Use GitOps: create a HelmRelease in the k8s/flux repo instead” rather than a bare denial and me having to explain it should do GitOps