Wildcard Permission Matching Failure for Curl Commands in Claude Code

Open 💬 21 comments Opened Jun 29, 2025 by fwends

Bug Description
Subject: Claude Code Permission System Not Working - Curl
Commands Still Being Blocked Despite Comprehensive
Wildcards

Bug Description:
The Claude Code permission system in
.claude/settings.local.json is not properly recognizing
wildcard patterns for curl commands. Despite having
extensive wildcard permissions including "", "Bash()",
"curl ", "curl", and "Bash(curl*)", curl commands are
still intermittently blocked and require user approval.

Expected Behavior:
All curl commands should execute without stopping when
comprehensive wildcard permissions are configured in the
settings file.

Actual Behavior:
Curl commands randomly stop execution and ask for
permission despite being explicitly whitelisted with
multiple wildcard patterns.

Configuration Details:

  • File: /Users/greg/codebase/.claude/settings.local.json
  • Platform: macOS (Darwin 24.5.0)
  • Claude Code Model: claude-sonnet-4-20250514

Permissions Configured:
{
"permissions": {
"allow": [
"*",
"Bash(*)",
"curl(*)",
"curl *",
"curl",
"Bash(curl)",
"Bash(curl*)",
"Bash(curl:*)",
"Bash(curl -*)",
"Bash(curl )",
"Bash(curl *)",
// ... plus 100+ specific curl patterns
],
"deny": []
}
}

Specific Examples That Still Get Blocked:

  • curl -X PUT "http://localhost:3351/api/..." -H

"Content-Type: application/json" -d '{"key":"value"}'

  • curl -s "http://localhost:3351/api/tasks" | jq
  • Various curl commands with different flag combinations

Impact:
This breaks automated workflows and forces manual
intervention for every curl command, making Claude Code
unusable for API-heavy development tasks.

Reproduction Steps:

  1. Configure comprehensive curl wildcards in

.claude/settings.local.json

  1. Attempt to run various curl commands through Claude

Code

  1. Observe that some commands still trigger permission

requests

Request:
Please fix the permission pattern matching system to
properly respect wildcard patterns, especially the global
"" and "Bash()" patterns which should theoretically
allow any command.

Environment Info

  • Platform: darwin
  • Terminal: vscode
  • Version: 1.0.35
  • Feedback ID: 8bd43805-6977-48a4-bd3a-01766423c25a

Errors

[{"error":"Error: Command failed: security find-generic-password -a $USER -w -s \"Claude Code\"\nsecurity: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.\n\n    at genericNodeError (node:internal/errors:983:15)\n    at wrappedFn (node:internal/errors:537:14)\n    at checkExecSyncError (node:child_process:892:11)\n    at execSync (node:child_process:964:15)\n    at TZ (file:///opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js:700:3921)\n    at file:///opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js:622:9014\n    at Q (file:///opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js:526:17199)\n    at VX (file:///opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js:622:8160)\n    at nS (file:///opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js:622:7241)\n    at f9 (file:///opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js:622:11408)","timestamp":"2025-06-29T08:18:41.487Z"}]

View original on GitHub ↗

21 Comments

BruceTrevarthen · 1 year ago

Yes, please, urgently, this slows down progress immensely - espcially in testing.

nsheff · 11 months ago

Related, I added this to "allow": in settings:

      "Edit(*plan.md)",

But claude doesn't recognize it; still prompts whenever needing to write to the file.

github-actions[bot] · 11 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/3428
  2. https://github.com/anthropics/claude-code/issues/4787
  3. https://github.com/anthropics/claude-code/issues/1569

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

fwends · 11 months ago

sigh

tonydehnke · 10 months ago

Running into the same issues too. Across multiple commands - on MacOS.

nbrustein · 10 months ago

This comment claims that claude does not support wildcards. But claude code seems to allow rules with wildcards itself when you select the option to allow things in the future. For example, claude code wrote this in my settings: "Bash(git commit:*)",

Is that comment just wrong?

sbs44 · 10 months ago
Permissions Configured: { "permissions": { "allow": [ "_", "Bash(_)", "curl(_)", "curl _", "_curl_", "Bash(_curl_)", "Bash(curl_)", "Bash(curl:_)", "Bash(curl -)", "Bash(curl )", "Bash(curl )", // ... plus 100+ specific curl patterns ], "deny": [] } }

@fwends – have you tried "Bash(curl:*)" (asterisk/wildcard after the colon)? That seems to work for me and follows the pattern that Claude Code uses in the settings.local.json if you select the "Yes, and don't ask again..." option for a given command prefix.

https://github.com/anthropics/claude-code/issues/3428#issuecomment-3067250906 claims that claude does not support wildcards. But claude code seems to allow rules with wildcards itself when you select the option to allow things in the future. For example, claude code wrote this in my settings: "Bash(git commit:*)", Is that comment just wrong?

@nbrustein – it definitely supports wildcards... I think he just worded his comment poorly. He's basically saying that to allow all Bash commands (which for the record, I have no clue why you would want to do that), you would use "Bash" instead of "Bash:*". But for a specific command (e.g., if you want to allow all lsof commands), you can use wildcards to do that ("Bash(lsof:*)").

monkeyden · 9 months ago
> Permissions Configured: { "permissions": { "allow": [ "_", "Bash(_)", "curl(_)", "curl _", "_curl_", "Bash(_curl_)", "Bash(curl_)", "Bash(curl:_)", "Bash(curl -)", "Bash(curl )", "Bash(curl )", // ... plus 100+ specific curl patterns ], "deny": [] } } @fwends – have you tried "Bash(curl:*)" (asterisk/wildcard after the colon)? That seems to work for me and follows the pattern that Claude Code uses in the settings.local.json if you select the "Yes, and don't ask again..." option for a given command prefix. > #3428 (comment) claims that claude does not support wildcards. But claude code seems to allow rules with wildcards itself when you select the option to allow things in the future. For example, claude code wrote this in my settings: "Bash(git commit:*)", > Is that comment just wrong? @nbrustein – it definitely supports wildcards... I think he just worded his comment poorly. He's basically saying that to allow all Bash commands (which for the record, I have no clue why you would want to do that), you would use "Bash" instead of "Bash:*". But for a specific command (e.g., if you want to allow all lsof commands), you can use wildcards to do that ("Bash(lsof:*)").

That's the same syntax I have for jq:
"Bash(jq:*)"

That particular one doesn't work but others seem to work perfectly fine:
"Bash(mv:*)"
"Bash(mkdir:*)"

fwends · 9 months ago

@monkeyden
@nbrustein
@sbs44

you know the issue is from 4 months ago right, now i just use bypass it's easier than messing about with claude options
i don't know if it got fixed, but it was not working no matter what i did and yes everything was tried to make it work, it was a claude issue not a problem between keyboard and chair!

nsheff · 9 months ago

I don’t have a concrete reproducible example yet, but I ran into a problem where if there’s even a slight syntax or command issue in the settings file, Claude silently ignores the entire file. This makes it look like it’s ignoring your allow permissions.

Through trial and error, I found that certain allow permission lines would cause Claude to fail to parse the file. If I removed just that line, the rest of the permissions worked as expected. The commands looked valid at first glance but still triggered the issue.

To debug, I used the /permissions command to check what had been granted. By progressively tweaking the file and removing lines, I was able to isolate the problematic entry.

Summary: a single invalid allow line causes Claude to silently discard all permissions from the settings file, with no error message or warning.

monkeyden · 9 months ago
@monkeyden @nbrustein @sbs44 you know the issue is from 4 months ago right, now i just use bypass it's easier than messing about with claude options i don't know if it got fixed, but it was not working no matter what i did and yes everything was tried to make it work, it was a claude issue not a problem between keyboard and chair!

I couldn't care less that it's from 4 mos ago. It's apparently still an issue in the latest release.

angelod1as · 9 months ago
> > Permissions Configured: { "permissions": { "allow": [ "_", "Bash(_)", "curl(_)", "curl _", "_curl_", "Bash(_curl_)", "Bash(curl_)", "Bash(curl:_)", "Bash(curl -)", "Bash(curl )", "Bash(curl )", // ... plus 100+ specific curl patterns ], "deny": [] } } > > > @fwends – have you tried "Bash(curl:*)" (asterisk/wildcard after the colon)? That seems to work for me and follows the pattern that Claude Code uses in the settings.local.json if you select the "Yes, and don't ask again..." option for a given command prefix. > > #3428 (comment) claims that claude does not support wildcards. But claude code seems to allow rules with wildcards itself when you select the option to allow things in the future. For example, claude code wrote this in my settings: "Bash(git commit:)", > > Is that comment just wrong? > > > @nbrustein – it definitely supports wildcards... I think he just worded his comment poorly. He's basically saying that to allow all Bash commands (which for the record, I have no clue why you would want to do that), you would use "Bash" instead of "Bash:*". But for a specific command (e.g., if you want to allow all lsof commands), you can use wildcards to do that ("Bash(lsof:*)"). That's the same syntax I have for jq: "Bash(jq:)" That particular one doesn't work but others seem to work perfectly fine: "Bash(mv:_)" "Bash(mkdir:_)"

I tried:

Bash(jq)
Bash(jq *)
Bash(jq:*)

in project and user settings. Nothing worked.

github-actions[bot] · 7 months ago

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

angelod1as · 7 months ago

Was this fixed? Why autoclose then?

tonydehnke · 7 months ago

Still seems broken or at least buggy for me

hongbo-miao · 6 months ago

I think this might be related to the requirement for both curl access and a whitelist of allowed domains.
Tested on Claude Code v2.0.76. This .claude/settings.local.json configuration works well for me.

{
  "permissions": {
    "allow": [
      "Bash(curl:*)",
      "WebFetch(domain:github.com)",
      "WebFetch(domain:raw.githubusercontent.com)"
    ]
  }
}

<img width="2758" height="1041" alt="Image" src="https://github.com/user-attachments/assets/5cb85ee7-46da-442a-a93d-d7c87dfb0c75" />

It does not ask any permissions again when I ask it to get some info from GitHub.

Hopefully it helps. ☺️

addebartek · 6 months ago

This does _not_ work in v2.1.9.
Having this in the permissions:

      "Bash(curl https://example.com*)",
      "WebFetch(domain:example.com)"

still prompts for curl usage.

It does appear to _sometimes_ work, which makes me scared about the security layer of the tool. The behaviour of the permissioning system should not depend on a dice roll.

monkeyden · 6 months ago
This does _not_ work in v2.1.9. Having this in the permissions: `` "Bash(curl https://example.com*)", "WebFetch(domain:example.com)" `` still prompts for curl usage. It does appear to _sometimes_ work, which makes me scared about the security layer of the tool. The behaviour of the permissioning system should not depend on a dice roll.

Concur. Some people (earlier CC versions?) it works for but this plagues me to this day. I have it configured to approve and I am never given the option to approve for the session. Neither for curl nor jq, and I have a lot of JSON.

EricSeastrand · 5 months ago

I really went deep on this one. Well, Claude did. I just told it what to do. I believe this is the same issue mentioned in this other GH Issue.

For Anthropic staff or their helper bots:

There's a bug in some validation logic that runs over each command and breaks things. There's a hack below that fixes it (allow-listing everything). The names of the minified functions are below, but of course will change before next release.

For Claude Users:

See "The Fix" below. Be careful about allow-listing everything. You have been warned.

The issue:

Even in skip permission mode, no matter how many times one presses "Yes, and don't ask again", it _will_ ask again, and again, and again. So I had Claude debug itself. It resorted to extracting the code from the npm package, beatifying it, then tracing through it to find the issue. Below is what it wrote up for me. The "fix" ... "worked" .. but of course blanket-allow-listing everything is not ideal.

Why --dangerously-skip-permissions Doesn't Work (and How to Fix It)

Claude Code v2.1.42 — February 2026

TL;DR

--dangerously-skip-permissions doesn't reliably bypass permission prompts. The flag sets an internal mode called bypassPermissions, but Bash commands go through a command injection security scanner that runs before the bypass mode is consulted. Certain command patterns trigger this scanner, causing a permission prompt regardless of the flag.

The fix: add explicit allow rules to ~/.claude/settings.json. These are checked at a different point in the permission flow — one that actually takes priority over the security scanner.

{
  "skipDangerousModePermissionPrompt": true,
  "permissions": {
    "defaultMode": "bypassPermissions",
    "allow": [
      "Bash(*)",
      "Read(*)",
      "Write(*)",
      "Edit(*)",
      "Glob(*)",
      "Grep(*)",
      "WebFetch(*)",
      "WebSearch(*)",
      "NotebookEdit(*)",
      "Task(*)"
    ]
  }
}

Add "mcp__<server-name>__*" entries for any MCP servers you use.

The Bug: How Permission Checks Actually Work

When Claude wants to run a tool, the permission system evaluates it through a chain of checks. The order matters, because the first definitive answer wins.

The Chain for Bash Commands

1. Check deny rules        → if match: DENY (stop)
2. Check ask rules         → if match: ASK (stop)
3. Run checkPermissions()  → Bash-specific logic:
   a. Check exact-match allow/deny/ask rules
   b. Check prefix-match allow/deny/ask rules
   c. Run command injection scanner (ob())    ← THE PROBLEM
   d. If prefix-match found an allow: ALLOW
   e. Default: ASK
4. If requiresUserInteraction(): ASK (stop)   ← only ExitPlanMode, AskUserQuestion
5. If mode === "bypassPermissions": ALLOW     ← --dangerously-skip-permissions
6. Check allow rules       → if match: ALLOW
7. Default: ASK

The bypass mode check is at step 5. The command injection scanner is at step 3c. When the scanner triggers, it returns {behavior: "ask"} from checkPermissions(). This bubbles up through step 3, and the flow reaches step 5 — but only if step 3 returned a "passthrough." When step 3 returns "ask," it's returned as the final result from checkPermissions(), and step 4 evaluates it. Since Bash doesn't set requiresUserInteraction, it falls through to step 5... except the result already has suggestions attached, and the permission UI kicks in.

The exact mechanics of why it short-circuits are buried in minified code, but the observable behavior is clear: complex Bash commands trigger prompts even with --dangerously-skip-permissions.

Why settings.json Allow Rules Fix It

The allow rules from settings.json are checked at step 3a (exact match) and step 3b (prefix match) — before the injection scanner at step 3c. Bash(*) matches as a wildcard prefix rule, so the check returns "allow" before ob() ever runs.

What Triggers the Command Injection Scanner

The scanner (ob()) runs 14 security checks on every Bash command. Here's what triggers a prompt:

Tier 1: Early Checks (run first, can allow or block)

| Check | Triggers On | Example |
|-------|-------------|---------|
| Empty command | Empty string | (auto-allowed) |
| Heredoc in substitution | $(cat <<EOF ...) | echo "$(cat <<'EOF' ...)" |
| Plain heredoc | <<-'DELIM' with quoted delimiter | cat <<'EOF' (auto-allowed) |
| Git commit | git commit with safe flags | git commit -m "msg" |

Tier 2: Security Checks (the ones that cause prompts)

| Check ID | Triggers On | Example That Would Prompt |
|----------|-------------|--------------------------|
| INCOMPLETE_COMMANDS | Starts with tab, flags, or operators | && echo hello, -rf / |
| JQ_SYSTEM_FUNCTION | jq with system keyword | jq 'system("rm -rf /")' file |
| JQ_FILE_ARGUMENTS | jq reading files in unsafe ways | various jq patterns |
| OBFUSCATED_FLAGS | Flags that don't match known-safe sets | sed with unusual flags |
| SHELL_METACHARACTERS | ;, \|, & in arguments | find . -name "*.tmp" -exec rm {} \; |
| DANGEROUS_VARIABLES | $VAR near pipes or redirects | echo $HOME \| grep user |
| NEWLINES | Newlines in unquoted content | Multi-line commands |
| COMMAND_SUBSTITUTION | $(), ${}, backticks | ` echo whoami |
| INPUT_REDIRECTION |
< with suspicious patterns | < /etc/passwd |
| OUTPUT_REDIRECTION |
> with suspicious patterns | > /etc/crontab |
| IFS_INJECTION |
$IFS or ${..IFS..} | $IFS in any command |
| GIT_COMMIT_SUBSTITUTION |
git commit with $() in message | git commit -m "$(date)" |
| PROC_ENVIRON_ACCESS |
/proc/*/environ | cat /proc/self/environ |
| MALFORMED_TOKEN_INJECTION | Unbalanced quotes, braces, brackets |
echo "hello` (missing close quote) |

The Pattern

Simple commands pass: ls, echo hello, git status, cat file.txt.

Commands that trigger prompts tend to involve:

  • Pipes: command1 | command2 (shell metacharacter check)
  • Command substitution: $(...) or backticks
  • Semicolons or chaining: cmd1 && cmd2, cmd1; cmd2
  • Variable expansion near operators: $VAR | grep, > $FILE
  • Multi-line commands: Anything with embedded newlines
  • Redirections with variables: < $INPUT, > $OUTPUT
  • Complex find/sed/jq invocations: Various heuristics

This is why the behavior seems random — simple echo or ls works, but a piped grep command prompts. The trigger is the content of the command, not anything about the tool or session configuration.

The --dangerously-skip-permissions Flag Isn't Entirely Broken

For non-Bash tools (Read, Write, Edit, Glob, Grep, MCP tools), the flag works correctly. These tools' checkPermissions() methods return "passthrough" or "allow," so the flow reaches step 5 where bypassPermissions takes effect.

It's specifically the Bash tool's command injection scanner that creates the gap. The scanner was clearly designed as a security-critical check that should override even bypass mode — but this was never documented, and the flag's description says "Bypass all permission checks."

Additional Issues

Sub-agents never inherit bypass mode

When Claude spawns sub-agents via the Task tool, they run with permissionMode: "default" or "acceptEdits". The parent's bypassPermissions mode is not propagated. This is a known architectural issue (GitHub #25526, #18950).

The settings.json fix partially helps here because the allow rules are loaded from disk by the sub-agent process, but the defaultMode setting may not be respected in all sub-agent contexts.

Plan mode and bypass mode are mutually exclusive

Shift+Tab cycles between modes as parallel states. Activating plan mode deactivates bypass (GitHub #5466). Worse, ExitPlanMode can be denied under bypass mode, creating a trap (GitHub #25627).

Statsig feature gate

A server-side feature gate (tengu_disable_bypass_permissions_mode) can silently disable bypass mode for your account. Check ~/.claude.json for cachedGrowthBookFeatures.tengu_disable_bypass_permissions_mode. If it's true, bypass mode is disabled regardless of any flags or settings.

Methodology

This analysis was performed by:

  1. Examining the running Claude process flags via ps
  2. Reading Claude's debug logs at ~/.claude/debug/
  3. Extracting and decompiling the bundled JavaScript from the Claude binary at ~/.local/share/claude/versions/2.1.42
  4. Tracing the permission evaluation chain through minified function names
  5. Cross-referencing with GitHub issues on anthropics/claude-code

Key functions in the binary (v2.1.42, names are minified):

  • aHB() — resolves CLI flags + settings into final permission mode
  • vS1() — per-tool-call permission evaluator
  • hHB() — Bash-specific permission checker
  • ob() — command injection scanner (14 checks)
  • cF() — main permission evaluation entry point
BlondeLegion · 4 months ago

Just adding onto this to say that it's a bit maddening when explore subagents are spawned and rack up dozens of permission requests despite my allow list. This singular issue means long-running tasks are impossible, given that you need to babysit the CLI to press enter almost all the time now.

yurukusa · 3 months ago

Wildcard permission matching fails for curl and other commands with complex arguments. A hook handles this reliably:

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
BASE=$(echo "$COMMAND" | sed 's/^\s*//' | awk '{print $1}')
if [ "$BASE" = "curl" ]; then
    URL=$(echo "$COMMAND" | grep -oE 'https?://[^ "]+' | head -1)
    if ! echo "$COMMAND" | grep -qE '\s-X\s+(POST|PUT|PATCH|DELETE)'; then
        jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"GET curl auto-approved"}}'
        exit 0
    fi
    ALLOWED_DOMAINS="api.github.com|registry.npmjs.org|pypi.org"
    if echo "$URL" | grep -qE "^https?://($ALLOWED_DOMAINS)"; then
        jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"curl to allowed domain"}}'
        exit 0
    fi
fi
exit 0
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/curl-allow.sh" }]
    }]
  }
}

The hook extracts the URL from the curl command and checks it against a domain allowlist. No wildcard pattern matching issues — the logic is explicit bash code.