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

> Source: <https://dev.to/aicoding-guide/claude-code-permissions-by-example-auto-allow-npm-scripts-confirm-git-push-block-force-push-4blb>
> Published: 2026-10-10 23:16:31+00:00

*Originally published at [https://aicoding-guide.com](https://aicoding-guide.com/en/posts/claude-code-permissions-examples/).*

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](https://aicoding-guide.com/en/posts/claude-code-settings-json-permissions/). For the file-oriented version of the same mechanism, see [Stop Claude Code reading your .env](https://aicoding-guide.com/en/posts/claude-code-deny-read-env/). Once scripts run unattended, a hook that lints after every edit keeps quality up: [Run lint and format automatically with Claude Code hooks](https://aicoding-guide.com/en/posts/claude-code-hooks-lint-format/).

`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 ask`Bash(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
