Skip to content

Copilot SDK rejects auto-granted Git subcommands and stalls after tool-denial guard #60364

Description

@jpmicrosoft

Summary

With the Copilot SDK driver, Git subcommands automatically granted by
safe-outputs.create-pull-request are rejected. After the fifth denial, the
driver records guard.tool_denials_exceeded and requests disconnect, but the
process can remain stalled until the GitHub Actions job timeout.

This prevented an otherwise validated change from reaching PR publication.
The workflow has not been changed to grant blanket Git/shell access or disable
the denial guard.

Environment and public evidence

  • gh-aw compiler/runtime: v0.88.4
  • Setup action: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a
  • Engine: copilot, copilot-sdk: true, max-tool-denials: 5,
    engine.harness.max-retries: 0
  • Compiled metadata records Copilot SDK 1.0.11.
  • Completed workflow run
  • Agent job
  • Downstream incident

The compiled GH_AW_COPILOT_SDK_TOOL_CONFIG.permissions.allowedTools includes
shell(git checkout:*) and shell(git branch:*). These are the commands the
framework's own
PR publication instructions
require.

1. Reproducible rejection of granted Git subcommands

The following standalone case uses the unmodified v0.88.4 permission helper
and its sibling bash_command_parser.cjs. It does not execute any Git command
or make network requests.

const { buildCopilotSDKPermissionHandler } =
  require("./copilot_sdk_permissions.cjs");

const handler = buildCopilotSDKPermissionHandler(
  { allowedTools: ["shell(git checkout:*)", "shell(git branch:*)"] },
  () => ({ kind: "approve-once" }),
);

for (const fullCommandText of [
  "git checkout -b automation/repro",
  "git branch --show-current",
]) {
  for (const identifiers of [["git"], [fullCommandText], []]) {
    const result = handler({
      kind: "shell",
      fullCommandText,
      commands: identifiers.map(identifier => ({ identifier })),
    });
    console.log({ fullCommandText, identifiers, actual: result.kind });
  }
}

Observed locally with Node v24.21.0: all six cases return reject.
Expected: approve-once, without granting other Git subcommands.
Separate controls confirmed that an exact shell(git status) grant succeeds
and ungranted git push remains rejected.

The
permission helper
compares the multiword prefix before :* with executable identifiers.
For these requests the candidates contain git or the entire command text,
not the git checkout/git branch prefix. The no-identifiers fallback also
reduces the command to git. The same matcher implementation is present in
v0.89.7, so an upgrade alone does not appear to address this case.

2. Denial guard records a stop but does not promptly exit

The hosted run recorded this sequence on September 12, 2026 (UTC):

Time Event
01:48:54 Offline sandbox compilation passed for every package.
01:57:56 Full Go test suite passed.
01:58:04 Denied a chain containing git checkout -b, git add, status, and diff.
01:58:07 Denied standalone git branch --show-current.
01:58:19 Denied standalone git checkout -b; counter reached 5/5.
01:58:19 Emitted guard.tool_denials_exceeded and logged stopping SDK session early.
02:03:51 onward Harness repeatedly reported no child output / possible hang.
02:47:34 Job timeout cancelled the agent step. Detection and safe outputs were skipped.

The first two denials were for ungranted sed calls; those rejections were
expected. The later Git rejections were not. The agent also failed to follow
the prompt instruction to stop after the first denial, which is why the
deterministic guard is important.

The guard event was emitted, but the process remained stalled for approximately
49 minutes afterward. This is hosted evidence, not a locally reproduced
disconnect-lifecycle test. The
session runner
records the catastrophic error and calls session.disconnect() while awaiting
sendAndWait; the exact SDK reason that the wait did not settle still needs
investigation.

Expected behavior / regression coverage

  • Match granted command/subcommand prefixes for SDK identifiers containing
    executable names, full command text, or no identifiers.
  • Keep ungranted commands denied, including in command chains. Do not solve
    this by changing git checkout:* to blanket git:*.
  • On the denial threshold, settle the driver with a nonzero exit within a
    bounded interval even if disconnect or the in-flight SDK request stalls.
  • Preserve the structured denial event, logs, failure reporting, and skipped
    publication for failed runs.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions