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.
Summary
With the Copilot SDK driver, Git subcommands automatically granted by
safe-outputs.create-pull-requestare rejected. After the fifth denial, thedriver records
guard.tool_denials_exceededand requests disconnect, but theprocess 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
github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7acopilot,copilot-sdk: true,max-tool-denials: 5,engine.harness.max-retries: 0The compiled
GH_AW_COPILOT_SDK_TOOL_CONFIG.permissions.allowedToolsincludesshell(git checkout:*)andshell(git branch:*). These are the commands theframework'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 commandor make network requests.
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 succeedsand ungranted
git pushremains rejected.The
permission helper
compares the multiword prefix before
:*with executable identifiers.For these requests the candidates contain
gitor the entire command text,not the
git checkout/git branchprefix. The no-identifiers fallback alsoreduces the command to
git. The same matcher implementation is present inv0.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):
git checkout -b,git add, status, and diff.git branch --show-current.git checkout -b; counter reached 5/5.guard.tool_denials_exceededand loggedstopping SDK session early.The first two denials were for ungranted
sedcalls; those rejections wereexpected. 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 awaitingsendAndWait; the exact SDK reason that the wait did not settle still needsinvestigation.
Expected behavior / regression coverage
executable names, full command text, or no identifiers.
this by changing
git checkout:*to blanketgit:*.bounded interval even if disconnect or the in-flight SDK request stalls.
publication for failed runs.