Steering the Vibe: Permissions
Once your agentic code-assist workflow is consistently producing acceptable code the bottleneck to productivity becomes the speed at which you can produce this code. One of the biggest slowdowns is when the tool prompts you - usually for permission to perform some task, and even just a few questions from the tool can severely cap sessions running in parallel. In this post we’ll look at refining Claude Code’s permissioning approach to reduce the number of questions asked and speed up the workflow.
Other posts in this series
- Steering the Vibe: Commits
- Steering the Vibe: Verify
- Steering the Vibe: Refactor
- Steering the Vibe: Review
- Steering the Vibe: Complexity
- Steering the Vibe: Permissions (this post)
My goto for code-assist is Claude Code and Opus 4.6, so I'll use terminology that relates to these tools, but the concepts within potentially apply to all.
Plan and accept edits
The basic permissions flow starts with Claude Code requesting permission to make edits in the repository - it doesn’t ask for permission to read in the repository because those are allowed by default. When in plan mode, it’s expected that no edits at all will occur, so this question is not usually asked unless the tool detects we need to start executing on the plan. So if our context is just the repository and our description of the task, we can expect zero or more clarification questions followed by some automatic reads and ultimately a prompt to review the plan.
We might not even do this in plan mode, and given a small enough task the first question we might be asked is to allow the first edit as the tool embarks on the task. We might accept the first edit, and review the second, but quickly this becomes tiresome as our confidence the correct approach is being taken increases. At this stage we’ll either choose the “Accept All Edits” option or switch it on beforehand via Shift+Tab to allow all edits without review. Over time understanding the tool brings a level of comfort that guides when we allow edits without being prompted first and the tool can progress through the task quicker without waiting for user input.
Bash calls
Even if the context is just the repository, the tool might need to use certain bash commands to traverse the codebase and by default each of these commands requires permission and is a stopping point for user input. When prompting for permission to run a bash command, one of the options is to allow all future calls without review and selecting this will add a line to the allow section of the .claude/settings.json which can also be managed via /permissions.
Over time you’ll start building up a list of allowed commands as settings.json grows. You might be happy with git status or git diff not requiring review but you might want to review the commit message in a git commit. By allowing the first two but not the third, you start to steer the tool towards a workflow that only prompts you for the things you care about. But the allow list starts to become unwieldy. There are a bunch of git * commands that we’re happy not to review, but we can’t add git * to the list because of destructive commands like git reset or git clean that we do want to review. Even commands like find can be dangerous if used with -delete or -exec.
Compound commands and Windows
Even as we build out our allow list, the tool still prompts us when compound commands are used. Commands like git status && git log --oneline -5 or git diff | head -20 prompt for review even if both sides are in the allow list. On Windows where shell support is a little shaky commands are often preceded with a cd to the absolute path of the repository, causing every command to be a compound command and therefore require review.
Solving via Hook
The PreToolUse hook gives us an intercept point to use our own logic to determine whether to allow a command or not with a fallback to the default permissioning flow:
json"hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "assist cli-hook" }] } ], }
I’ve extended assist to expose a cli-hook command that we can call from the PreToolUse hook. One of the most basic capabilities of cli-hook is the splitting of compound commands into their individual parts and subsequent matching of those parts against the allow list. Provided all parts of the compound command are in the allow list, the tool will no longer prompt for review.
Compound commands however might have many parts and it becomes difficult to reason about why a command is being blocked or allowed. To solve this the assist cli-hook check command will output the breakdown of the command and which parts are being blocked or allowed.
bashassist cli-hook check "gh api --method POST /some/write | grep 'allowed'" not approved (unrecognized: gh api --method POST /some/write)
bashassist cli-hook check "gh api /some/read | grep 'allowed'" approved gh api /some/read -> Read-only gh api command grep 'allowed' -> Allowed by settings: grep
Generating the allow list
CLI tools GitHub’s gh and Azure’s az have a huge number of subcommands. Deciding which subcommands to allow and adding them to the allow list for these and other CLIs is a huge chore, and without adding them Claude Code will continuously find new ways to prompt for review. To solve for this I’ve implemented assist cli-hook add <cli> which traverses a CLI’s nested subcommands. Based on a set of verbs, we split subcommands into read or write and allow all reads by default.
This gives us a huge productivity boost as by default read commands do not prompt for review. However gh api specifically is a little more tricky. That command is a passthrough to the GitHub REST API or GraphQL API (via gh api graphql). In the case of REST API calls we can consider the http method to determine read or write and for GraphQL calls we can check for the presence of query or mutation keywords. This functionality is built directly into assist’s cli-hook and so calls to gh api are automatically classified as read or write and allowed or blocked as appropriate.
Synchronizing across environments
Even in the case that we don’t automate our allow lists, sharing them across environments is another bottleneck with settings.json or settings.local.json having separate instances across repos and machines. While I do have repo-specific permissions, even allowing writes in some cases, I want the reads to be easily shared across everywhere I use Claude Code and assist offers a number of approaches to solve this.
assist ships a settings.json that has shared, machine-level configuration. The source of truth for this file is the git repository and it needs to be copied to ~/.claude/settings.json to be accessed by Claude Code. assist update automates the updating of both assist and that file onto a local machine and assist sync moves the local copy into ~/.claude.
While smaller CLIs like gh or git can be traversed and categorized quickly, az in particular is gigantic and takes some time to traverse. Because of this, I also ship the output of assist cli-hook add with assist itself, and running assist update will also update that list so I don’t need to run CLI traversals on every machine.