Skip to content

fix(engines): normalize wildcard bash commands to canonical tool permission syntax#31599

Merged
pelikhan merged 3 commits into
mainfrom
copilot/fix-tool-permission-denied-errors
May 12, 2026
Merged

fix(engines): normalize wildcard bash commands to canonical tool permission syntax#31599
pelikhan merged 3 commits into
mainfrom
copilot/fix-tool-permission-denied-errors

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 11, 2026

Bug Fix

What was the bug?

The release agent (and other workflows importing shared/community-attribution.md) was hitting repeated "Permission denied" errors for jq, awk, sed, and bash invocations. The shared import provides these as jq *, sed *, etc., which compiled to --allow-tool shell(jq *). The Copilot CLI uses prefix matchingshell(jq *) only allows commands literally starting with jq * (literal asterisk), so every real jq invocation was denied.

How did you fix it?

Added a shared normalizeBashCommand() helper in pkg/workflow/engine_helpers.go that strips the trailing " *" suffix from bash tool entries, converting wildcard forms to their canonical prefix form:

  • "jq *""jq"
  • "gh issue list *""gh issue list"

Applied the normalization in all three agentic engines before they wrap the command in their engine-specific permission format:

Engine Before After
Copilot shell(jq *) shell(jq)
Claude Bash(jq *) Bash(jq)
Gemini run_shell_command(jq *) run_shell_command(jq)

This means imported shared workflows like community-attribution.md (which use "jq *", "sed *", "cat *", etc.) now emit the canonical prefix form that each engine's permission matcher actually uses, without requiring every consumer to add redundant bare entries.

The full-wildcard sentinels "*" and ":*" (which signal "allow all shell commands") are handled separately and are unaffected by this change.

Testing

  • Added TestNormalizeBashCommand unit tests for the new helper covering plain commands, cmd * patterns, multi-word commands, and sentinel values.
  • Added wildcard normalization test cases to the Copilot, Claude, and Gemini engine tool tests.
  • Updated one pre-existing test that expected the old non-normalized shell(python3 *) form.
  • Regenerated 24 affected lock files; all shell(cmd *) patterns are replaced with canonical shell(cmd) forms.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Fix tool permission denied errors in release.md bash configuration fix(release): add missing bash tools to unblock jq, awk, sed, bash in agent May 11, 2026
Copilot AI requested a review from pelikhan May 11, 2026 23:29
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot all agentic engines should normalize wildcard bash command to the canonical syntax they sipport. Eg jq * -> shell(jq)

Copilot AI and others added 2 commits May 12, 2026 01:21
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…prefix form

Add normalizeBashCommand() helper in engine_helpers.go that strips the
" *" suffix from bash tool entries (e.g. "jq *" → "jq").  Apply it in
all three agentic engines before wrapping the command in their respective
permission formats:

  Copilot:  shell(jq *) → shell(jq)
  Claude:   Bash(jq *)  → Bash(jq)
  Gemini:   run_shell_command(jq *) → run_shell_command(jq)

This means imported shared workflows like community-attribution.md (which
use "jq *", "sed *", "cat *", etc.) now emit the canonical prefix form
that Copilot CLI prefix-matching actually uses, without requiring every
consumer to add redundant explicit bare entries.

Updated test expectations and regenerated affected lock files.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 12, 2026

@copilot all agentic engines should normalize wildcard bash command to the canonical syntax they sipport. Eg jq * -> shell(jq)

Done in the latest commit. Added a shared normalizeBashCommand() helper in engine_helpers.go that strips the trailing " *" suffix from bash tool entries, and applied it in all three agentic engines before they wrap the command in their respective permission formats:

  • Copilot: shell(jq *)shell(jq)
  • Claude: Bash(jq *)Bash(jq)
  • Gemini: run_shell_command(jq *)run_shell_command(jq)

Imported shared workflows like community-attribution.md (which use "jq *", "sed *", etc.) now emit the canonical prefix form without requiring consumers to add redundant bare entries. Also regenerated the 24 affected lock files.

Copilot AI changed the title fix(release): add missing bash tools to unblock jq, awk, sed, bash in agent fix(engines): normalize wildcard bash commands to canonical tool permission syntax May 12, 2026
@pelikhan pelikhan marked this pull request as ready for review May 12, 2026 01:41
Copilot AI review requested due to automatic review settings May 12, 2026 01:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes tool-permission mismatches across Copilot/Claude/Gemini engines by normalizing bash tool entries written with a trailing " *" wildcard into canonical prefix-matching forms, preventing “Permission denied” for common commands imported from shared workflows.

Changes:

  • Added normalizeBashCommand() helper and unit tests to strip trailing " *" from per-command bash tool entries.
  • Applied normalization across Copilot, Claude, and Gemini tool permission generation + expanded engine test cases.
  • Regenerated affected workflow lock files and removed an unused action pin entry.
Show a summary per file
File Description
pkg/workflow/engine_helpers.go Adds shared normalizeBashCommand() helper.
pkg/workflow/engine_helpers_test.go Adds unit tests for the normalization helper.
pkg/workflow/copilot_engine_tools.go Normalizes bash tool entries before emitting shell(...) allowlist items.
pkg/workflow/copilot_engine_test.go Updates/extends Copilot engine tool permission tests for normalization.
pkg/workflow/claude_tools.go Normalizes bash tool entries before emitting Bash(...) allowlist items.
pkg/workflow/claude_engine_tools_test.go Adds Claude tool allowlist normalization test cases.
pkg/workflow/gemini_tools.go Normalizes bash tool entries before emitting run_shell_command(...) items.
pkg/workflow/gemini_engine_test.go Adds Gemini tool-core normalization test cases.
pkg/workflow/imports_inputs_test.go Updates expectation to reflect normalized bash tool output in lock content.
pkg/workflow/data/action_pins.json Removes an unused pinned action entry.
pkg/actionpins/data/action_pins.json Removes an unused pinned action entry (mirrored dataset).
.github/aw/actions-lock.json Removes an unused pinned action entry (GitHub-side lock).
.github/workflows/release.md Adds explicit bash tool entries for the release workflow.
.github/workflows/release.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/workflow-skill-extractor.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/visual-regression-checker.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/unbloat-docs.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/stale-pr-cleanup.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/smoke-workflow-call.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/smoke-workflow-call-with-inputs.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/smoke-multi-pr.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/scout.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/safe-output-health.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/go-logger.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/draft-pr-cleanup.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/discussion-task-miner.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/dev-hawk.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/daily-syntax-error-quality.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/daily-safe-output-optimizer.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/daily-safe-output-integrator.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/daily-rendering-scripts-verifier.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/daily-geo-optimizer.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/daily-community-attribution.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/copilot-opt.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/copilot-cli-deep-research.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/blog-auditor.lock.yml Regenerated lock file reflecting normalized tool permissions.
.github/workflows/auto-triage-issues.lock.yml Regenerated lock file reflecting normalized tool permissions.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 37/37 changed files
  • Comments generated: 3

Comment on lines +491 to +559
func TestNormalizeBashCommand(t *testing.T) {
tests := []struct {
name string
input string
expectedCmd string
expectedChanged bool
}{
{
name: "plain command unchanged",
input: "jq",
expectedCmd: "jq",
expectedChanged: false,
},
{
name: "command with space-star suffix is stripped",
input: "jq *",
expectedCmd: "jq",
expectedChanged: true,
},
{
name: "multi-word command with space-star suffix is stripped",
input: "gh issue list *",
expectedCmd: "gh issue list",
expectedChanged: true,
},
{
name: "command with arguments but no wildcard unchanged",
input: "jq . /tmp/file.json",
expectedCmd: "jq . /tmp/file.json",
expectedChanged: false,
},
{
name: "lone star is not stripped (handled as full-wildcard sentinel)",
input: "*",
expectedCmd: "*",
expectedChanged: false,
},
{
name: "colon-star is not stripped (handled as full-wildcard sentinel)",
input: ":*",
expectedCmd: ":*",
expectedChanged: false,
},
{
name: "sed with space-star suffix is stripped",
input: "sed *",
expectedCmd: "sed",
expectedChanged: true,
},
{
name: "awk with space-star suffix is stripped",
input: "awk *",
expectedCmd: "awk",
expectedChanged: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotCmd, gotChanged := normalizeBashCommand(tt.input)
if gotCmd != tt.expectedCmd {
t.Errorf("normalizeBashCommand(%q) cmd = %q, want %q", tt.input, gotCmd, tt.expectedCmd)
}
if gotChanged != tt.expectedChanged {
t.Errorf("normalizeBashCommand(%q) changed = %v, want %v", tt.input, gotChanged, tt.expectedChanged)
}
})
}
}
Comment on lines 276 to 285
// Add individual bash commands with Bash() prefix
for _, cmd := range bashCommands {
if cmdStr, ok := cmd.(string); ok {
allowedTools = append(allowedTools, fmt.Sprintf("Bash(%s)", cmdStr))
// Normalize trailing " *" wildcard (e.g. "jq *" → "jq") so that
// all engines emit the canonical prefix form (Bash(jq)) regardless
// of whether the command was written with or without the wildcard.
normalized, _ := normalizeBashCommand(cmdStr)
allowedTools = append(allowedTools, fmt.Sprintf("Bash(%s)", normalized))
}
}
cli-proxy: true
bash:
- jq
- bash
@pelikhan pelikhan merged commit 092ce8b into main May 12, 2026
4 checks passed
@pelikhan pelikhan deleted the copilot/fix-tool-permission-denied-errors branch May 12, 2026 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants