cd /news/ai-tools/claude-code-permissions-by-example-a… · home › topics › ai-tools › article
[ARTICLE · art-148941] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

Claude Code permissions by example, auto-allow npm scripts, confirm git push, block force push

A developer published a practical guide showing how to configure Claude Code's permissions in a project's .claude/settings.json using allow, ask and deny layers, so that npm scripts and read-only git commands run silently, git push and npm install prompt for confirmation, and force pushes are refused outright. The writeup explains that rules are evaluated deny-first, then ask, then allow, and that the ':*' suffix in a rule like Bash(npm run:*) is a prefix match. It also warns that npx can run arbitrary packages and that ask rules outrank allow rules, so broad allow entries must be paired with targeted ask rules for install commands.

by read6 min views1 publishedOct 10, 2026

Originally published at https://aicoding-guide.com.

When you let Claude Code drive, you usually want three lines drawn. Scripts you wrote yourself, such as npm run lint and npm run test, should run without a prompt. git push reaches the remote, so you want one last look right before it. git push --force should never run at all.

All three fit in a single permissions block in settings.json, using the three layers allow, ask and deny. This article shows the finished configuration first, then explains each layer and the places people trip.

Key point

What you will learn

  • A complete settings.json that combines allow, ask and deny
  • What the :* prefix match means, and how to keep install commands out of an allow rule- Why ask outranks allow, and why "don't ask again" still prompts
  • How to cover every spelling of a force push ( -f, --force-with-lease, reordered options) with deny

Put this in the project's .claude/settings.json. It is written to be shared with a team.

{
  "permissions": {
    "allow": [
      "Bash(npm run:*)",
      "Bash(npm test)",
      "Bash(npx tsc:*)",
      "Bash(npx vitest:*)",
      "Bash(npx eslint:*)",
      "Bash(npx prettier:*)",
      "Bash(git status)",
      "Bash(git diff:*)",
      "Bash(git log:*)",
      "Bash(git add:*)",
      "Bash(git commit:*)"
    ],
    "ask": [
      "Bash(git push:*)",
      "Bash(npm install:*)",
      "Bash(npm i:*)",
      "Bash(npm uninstall:*)"
    ],
    "deny": [
      "Bash(git push --force:*)",
      "Bash(git push -f:*)",
      "Bash(git push --force-with-lease:*)",
      "Bash(git push * --force:*)"
    ]
  }
}

With this in place, scripts and the read-only Git commands plus commits run silently, git push and npm install open a dialog, and a force push is refused.

Glossary

Rule precedence: rules are evaluated deny, then ask, then allow, and the first match in that order decides. Specificity does not change the order. An allow rule for Bash(git:*) does not stop an ask rule for Bash(git push:*) from prompting, and a deny match is refused in every permission mode.

In Bash(npm run:*), :* is a prefix match: "starts with npm run, anything may follow." It matches arguments too, such as npm run test -- --watch. npm install begins differently, so it is not swept in. Put the * after the subcommand; a rule with the * before it, like Bash(npm * test), triggers a startup warning.

npx can run any package, so the example allows only the ones you use rather than the whole program. Do not put Bash(npx:*) in ask: ask is evaluated before allow, so the npx tsc you just allowed would start prompting too. Leaving it out means "the ones I allowed run silently, any other npx follows the default behavior."

Tool Running scripts Install commands (ask)
npm Bash(npm run:*) Bash(npm install:*) ,Bash(npm i:*)
pnpm Bash(pnpm run:*) ,Bash(pnpm test) ,Bash(pnpm lint) Bash(pnpm add:*) ,Bash(pnpm install:*)
yarn Bash(yarn run:*) ,Bash(yarn test) Bash(yarn add:*) ,Bash(yarn install)
bun Bash(bun run:*) Bash(bun add:*) ,Bash(bun install:*)

pnpm and yarn let you drop run, as in pnpm lint, so either list the scripts you use or allow the whole program and stop the install commands with ask rules. Because ask is evaluated first, that combination is safe:

{
  "permissions": {
    "allow": ["Bash(pnpm:*)"],
    "ask": ["Bash(pnpm add:*)", "Bash(pnpm install:*)", "Bash(pnpm remove:*)"]
  }
}
{
  "permissions": {
    "allow": ["Bash(pytest:*)", "Bash(python -m pytest:*)", "Bash(ruff:*)", "Bash(mypy:*)", "Bash(uv run:*)"],
    "ask": ["Bash(pip install:*)", "Bash(uv add:*)"]
  }
}

An ask rule means "prompt me even if an allow rule matches." Automate the read-only Git commands and commits with allow, put Bash(git push:*) in ask, and only the push stops.

The dialog at push time offers these options:

Option Effect
Allow once Runs this one call
Don't ask again Appends an allow rule to settings.local.json , but you are still prompted next time while the ask rule remains
Deny Does not run, and lets you tell Claude why

That "don't ask again" leaves the ask rule in place is intended. It keeps you from undoing "I always review pushes" with one keystroke. To stop the prompts, remove the ask line from the settings file.

If pushes to your own working branches should go through, scope both the allow and the ask rule by branch:

{
  "permissions": {
    "allow": ["Bash(git push origin feature/*:*)"],
    "ask": ["Bash(git push origin main:*)", "Bash(git push origin develop:*)"]
  }
}

This depends on Claude spelling the command as git push origin feature/xxx. A bare git push matches neither rule and falls through to your defaultMode. If you value reliability over convenience, skip the branch scoping and keep one "always confirm pushes" rule.

Deny is evaluated before ask and allow, and it applies in every permission mode, including bypassPermissions, where allow rules have no effect. A deny in the project's settings.json cannot be undone by an allow in someone's settings.local.json, which is why team-wide prohibitions belong here.

Bash rules match the command text, so each spelling needs its own line:

Command Pattern that matches it
git push --force origin main Bash(git push --force:*)
git push -f origin main Bash(git push -f:*)
git push origin main --force Bash(git push * --force:*)
git push --force-with-lease Bash(git push --force-with-lease:*)

--force-with-lease is safer than a bare --force, but it still overwrites the remote. If your team allows it, move that one line into ask.

A deny rule is a safety net, not a wall

cd repo && git push --force, or a push buried in a shell script, does not start with the text your rule matches. For a branch that genuinely must not be rewritten, set branch protection on the remote (GitHub's "do not allow force pushes") and treat the deny rule as a way to reduce local accidents.

The same approach covers other commands that destroy work:

{
  "permissions": {
    "deny": ["Bash(git reset --hard:*)", "Bash(git checkout -- .:*)", "Bash(git clean -f:*)"]
  }
}

/permissions and check that each list shows the lines you wrote and the file each came from.npm run test goes without a prompt, allow works.npm install lodash prompts, ask works.git push --force origin test-branch. It should be refused. How settings.json and settings.local.json divide the work, how the scopes override each other and how to pick a defaultMode are covered in the parent article, Claude Code permissions in settings.json and settings.local.json. For the file-oriented version of the same mechanism, see Stop Claude Code reading your .env. Once scripts run unattended, a hook that lints after every edit keeps quality up: Run lint and format automatically with Claude Code hooks.

Bash(npm run:*) allows every npm script in one rule; npm install is a different prefix. When allowing a whole program such as pnpm or npx, stop the install commands with askBash(git push:*) in ask forces a prompt right before every push, and "don't ask again" does not remove it--force, -f, --force-with-lease and the reordered form separately. Deny applies in every mode and cannot be undone by personal settings

── more in #ai-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-code-permissi…] indexed:0 read:6min 2026-10-10 · —