Skip to content

Conversation

@github-actions
Copy link
Contributor

Fix: Bash Syntax Error in Copilot Workflows (Issue #2485)

Problem

Copilot smoke tests were failing with bash syntax errors when executing workflows with shell tool permissions:

/home/runner/work/_temp/xxx.sh: line 5: syntax error near unexpected token `('

The workflow compiler generated commands like:

"npx `@github/copilot` --allow-tool 'shell(cat)' --allow-tool 'shell(date)' ..."

When bash executes this, the single quotes inside double quotes are treated as literal quote characters (not delimiters), leaving the parentheses unprotected. Bash then interprets (cat) as subshell syntax, causing a syntax error.

Solution (Option 1 from #2485)

Escape parentheses in shellEscapeCommandString function by adding:

// Escape parentheses (to prevent subshell interpretation inside double quotes)
escaped = strings.ReplaceAll(escaped, "(", "\\(")
escaped = strings.ReplaceAll(escaped, ")", "\\)")

Now the generated command becomes:

"npx `@github/copilot` --allow-tool 'shell\\(cat\\)' --allow-tool 'shell\\(date\\)' ..."

The escaped parentheses \( and \) are treated as literal characters by bash, preventing subshell interpretation.

Changes Made

Modified Files

  1. pkg/workflow/shell.go

    • Added parentheses escaping to shellEscapeCommandString function
    • Updated function documentation to mention parentheses escaping
  2. pkg/workflow/shell_test.go

    • Updated test expectations to include escaped parentheses
    • Modified 3 test cases:
      • "command with single-quoted arguments"
      • "command with dollar sign (command substitution)"
      • "complex copilot command"

Testing

All unit tests updated to expect the new escaping behavior. The fix affects only the shellEscapeCommandString function, which is specifically used when wrapping commands in double quotes for programs like AWF.

Impact

  • Fixes: All Copilot smoke tests with shell tool permissions
  • Scope: Only affects firewall-enabled Copilot workflows (where AWF wrapper is used)
  • Breaking Changes: None - this is a bug fix for incorrect shell escaping

Related

  • Fixes #2485
  • Pattern ID: COPILOT_BASH_SHELL_SYNTAX_PARENTHESES
  • First occurred: 2025-10-26T00:12:23Z (Run #18810304059)
  • Affected workflows: Smoke Copilot, Smoke Copilot Firewall

Implementation: Option 1 - Escape parentheses in compiler (as recommended in #2485)

AI generated by Q

Implements Option 1 from issue #2485: Escape parentheses in shellEscapeCommandString
to prevent bash from interpreting them as subshell syntax when commands are wrapped
in double quotes.

The issue occurred when tool names like 'shell(cat)' were wrapped in single quotes,
then the entire command was wrapped in double quotes. Bash treats single quotes as
literal characters inside double quotes, leaving parentheses unprotected.

Changes:
- Added parentheses escaping to shellEscapeCommandString function
- Updated tests to expect escaped parentheses in command strings

Fixes #2485
@pelikhan
Copy link
Contributor

@copilot backticks around @github/copilot are not needed. Fix and recompile.

Copy link
Contributor

Copilot AI commented Oct 26, 2025

@pelikhan I've opened a new pull request, #2494, to work on those changes. Once the pull request is ready, I'll request review from you.

// Escape parentheses (to prevent subshell interpretation inside double quotes)
// BUT preserve command substitution syntax: \$(...) should remain as \$(...)
// We need to escape ( and ) except when they immediately follow \$ (which was $ before escaping)
result := make([]byte, 0, len(escaped)*2)

Check failure

Code scanning / CodeQL

Size computation for allocation may overflow High

This operation, which is used in an
allocation
, involves a
potentially large value
and might overflow.
This operation, which is used in an
allocation
, involves a
potentially large value
and might overflow.
This operation, which is used in an
allocation
, involves a
potentially large value
and might overflow.
This operation, which is used in an
allocation
, involves a
potentially large value
and might overflow.
This operation, which is used in an
allocation
, involves a
potentially large value
and might overflow.

Copilot Autofix

AI 8 days ago

The best solution is to introduce a length check in shellEscapeCommandString before making the allocation for result. We should defensively check that len(escaped) is below a reasonable safe maximum (e.g., 64MB, which is standard for preventing overflow like in the CodeQL example), and fail gracefully if it exceeds that value. The check should return an empty quoted shell string or some visually obvious error string if the length is excessive, or, better, panic with a clear, descriptive error (or perhaps log and return a safe error). Since only pkg/workflow/shell.go is shown and permitted for editing, these checks must be implemented there.

Specifically:

  • Edit shellEscapeCommandString in pkg/workflow/shell.go to check that len(escaped) is below a threshold (e.g., 6410241024); otherwise, return an error string.
  • Optionally, you could also log the error to aid future diagnostics, but as logging facilities may not be imported here, prefer minimal intervention.
  • You might define a const for the threshold for clarity and future maintainability.
Suggested changeset 1
pkg/workflow/shell.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pkg/workflow/shell.go b/pkg/workflow/shell.go
--- a/pkg/workflow/shell.go
+++ b/pkg/workflow/shell.go
@@ -44,6 +44,7 @@
 // Special case: Parentheses immediately following $ (i.e., $(...)) are NOT escaped
 // to preserve command substitution syntax.
 func shellEscapeCommandString(cmd string) string {
+	const MaxCmdLength = 64 * 1024 * 1024 // 64MB safeguard
 	// Escape backslashes first (must be done before other escapes)
 	escaped := strings.ReplaceAll(cmd, "\\", "\\\\")
 	// Escape double quotes
@@ -56,6 +57,10 @@
 	// Escape parentheses (to prevent subshell interpretation inside double quotes)
 	// BUT preserve command substitution syntax: \$(...) should remain as \$(...)
 	// We need to escape ( and ) except when they immediately follow \$ (which was $ before escaping)
+	if len(escaped) > MaxCmdLength {
+		// Defensive fail: do not attempt to allocate, return a safe shell string
+		return "\"\"" // Or optionally return an error string
+	}
 	result := make([]byte, 0, len(escaped)*2)
 	for i := 0; i < len(escaped); i++ {
 		ch := escaped[i]
EOF
@@ -44,6 +44,7 @@
// Special case: Parentheses immediately following $ (i.e., $(...)) are NOT escaped
// to preserve command substitution syntax.
func shellEscapeCommandString(cmd string) string {
const MaxCmdLength = 64 * 1024 * 1024 // 64MB safeguard
// Escape backslashes first (must be done before other escapes)
escaped := strings.ReplaceAll(cmd, "\\", "\\\\")
// Escape double quotes
@@ -56,6 +57,10 @@
// Escape parentheses (to prevent subshell interpretation inside double quotes)
// BUT preserve command substitution syntax: \$(...) should remain as \$(...)
// We need to escape ( and ) except when they immediately follow \$ (which was $ before escaping)
if len(escaped) > MaxCmdLength {
// Defensive fail: do not attempt to allocate, return a safe shell string
return "\"\"" // Or optionally return an error string
}
result := make([]byte, 0, len(escaped)*2)
for i := 0; i < len(escaped); i++ {
ch := escaped[i]
Copilot is powered by AI and may make mistakes. Always verify output.
@github-actions
Copy link
Contributor Author

🔍 Smoke Test Investigation - Run #7

Summary

NEW ISSUE DISCOVERED: The fix in this PR introduces backslash escaping (shell\(cat\)) which causes Copilot CLI to reject the tool permission rules with error: Invalid rule format: shell\(cat\). This is a validation error in Copilot CLI, not a bash syntax error.

Failure Details

  • Run: 18812954043
  • Commit: 6ce8853
  • Branch: fix-shell-parentheses-escaping-b8e5cf674ee27756
  • Trigger: workflow_dispatch
  • Duration: 1.1 minutes
  • Conclusion: failure

Root Cause Analysis

The original issue was a bash syntax error when parentheses in tool names like 'shell(cat)' were interpreted as subshell syntax.

This PR attempted to fix it by escaping parentheses in the shell escape function, resulting in: 'shell\(cat\)'

However, this creates a new problem: The Copilot CLI's rule parser validates tool permission formats using regex pattern:

/^([a-zA-Z0-9_-]+)(\(([^)]+)\))?$/

When the CLI receives --allow-tool 'shell\(cat\)', it sees the literal backslash characters \( and \) which don't match the expected pattern, causing:

Error: Invalid rule format: shell\(cat\)
    at QJe ((redacted))

Key Finding: The issue occurs at line 265 in the agent stdio log:

--allow-tool 'shell\(cat\)' --allow-tool 'shell\(date\)' ...

The backslashes are being passed through to Copilot CLI as literal characters, not consumed by bash as escape sequences.

Failed Jobs and Errors

Agent Job: Failed in 48 seconds

  • Primary Error: Invalid rule format: shell\(cat\) at /tmp/gh-aw/aw-mcp/logs/run-18812954043/agent-stdio.log:271
  • Location: Copilot CLI argument parsing (index.js:841)
  • Exit Code: 1

Downstream Jobs: All skipped (detection, create_issue, missing_tool)

Investigation Findings

This failure reveals a critical incompatibility between three layers:

  1. Workflow Compiler (Go code): Escapes parentheses → shell\(cat\)
  2. Bash Script Execution: Preserves backslashes in single quotes → 'shell\(cat\)' stays as-is
  3. Copilot CLI Parser: Expects unescaped parentheses → rejects shell\(cat\)

The Problem: When you use single quotes in bash, backslashes are literal characters, not escape sequences. So:

  • Input to bash: 'shell\(cat\)'
  • Bash passes to Copilot: shell\(cat\) (backslashes preserved)
  • Copilot expects: shell(cat) (no backslashes)

Comparison with Original Issue

Aspect Original Issue (#2485) This PR's Issue
Error Location Bash script parser Copilot CLI argument parser
Error Type Syntax error Validation error
Error Message syntax error near unexpected token '(' Invalid rule format: shell\(cat\)
Exit Code 2 1
Failure Point Before Copilot starts During Copilot initialization
Root Cause Bash interprets ( as subshell Copilot rejects \( in rule format

Recommended Actions

The escaping approach needs refinement. Here are the options:

Option 1: Double Quoting Strategy (Recommended)

Use double quotes with proper escaping in the workflow compiler:

--allow-tool "shell(cat)"  # Double quotes protect parentheses from bash

Pros:

  • Simple and direct
  • No backslashes needed
  • Copilot receives clean format

Cons:

  • Need to escape any internal quotes or dollar signs

Option 2: Conditional Escaping for Firewall Wrapper

Only escape when wrapping in AWF's docker command (which uses double quotes), but not in single-quoted contexts:

# For AWF wrapper (double-quoted context):
"... --allow-tool 'shell\\(cat\\)' ..."

# For direct execution (single-quoted context):
... --allow-tool 'shell(cat)' ...

Pros:

  • Handles both contexts correctly
  • Targeted fix

Cons:

  • More complex logic

Option 3: Fix AWF Entrypoint Parsing

Modify the AWF container's entrypoint to handle the nested quoting correctly so parentheses don't trigger subshell interpretation.

Pros:

  • Fixes the issue at the right layer
  • No changes to workflow compiler

Cons:

  • Requires AWF container changes

Prevention Strategies

  1. Add integration tests that run compiled workflows through actual bash parsing
  2. Test with Copilot CLI validation to catch argument parsing errors
  3. Document quoting strategy for command generation in AWF wrapper
  4. Add test cases for tool names with special characters: (), [], {}, $, etc.
  5. Validate escaping at multiple layers (compiler → bash → CLI)

Historical Context

This is the 3rd iteration of this issue:

  1. Run 18810304059 (Oct 26, 00:12): First bash syntax error discovered
  2. Run 18810373316 (Oct 26, 00:18): Confirmed systemic across Copilot workflows
  3. Run 18812954043 (Oct 26, 04:26): NEW - Backslash escaping breaks Copilot CLI parsing

Pattern ID: COPILOT_BASH_SHELL_SYNTAX_PARENTHESES (now evolved into validation error)

Next Steps

  1. Review PR Fix bash syntax error by escaping parentheses in Copilot shell commands #2494 - Check if Copilot's implementation handles this differently
  2. Test double-quote approach locally before updating this PR
  3. Verify with AWF wrapper to ensure proper quoting at all layers
  4. Update pattern database with this new validation error variant

Investigation completed at: 2025-10-26T04:30:42Z
Pattern: COPILOT_BASH_SHELL_SYNTAX_PARENTHESES → COPILOT_CLI_INVALID_RULE_FORMAT
Severity: Critical - Blocks workflow execution
Recurrence: 3rd iteration of shell tool permission parsing issue

AI generated by Smoke Detector - Smoke Test Failure Investigator

@pelikhan
Copy link
Contributor

@Mossaka might need to do changes in awf to solve this.

@pelikhan pelikhan closed this Oct 30, 2025
@pelikhan pelikhan deleted the fix-shell-parentheses-escaping-b8e5cf674ee27756 branch October 30, 2025 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants