Summary
When the agent repeatedly hits permission denials or tool errors (e.g. trying to read outside the workspace), it often retries the same failing path instead of adapting. A nudge after 2–3 consecutive failures would remind it that it may be in a sandboxed environment and guide it toward an alternative approach.
Motivation
Observed in practice: the agent attempted to read files under ~/.cache/ (not in the allowed directory list), triggered two permission dialogs, both were denied, and the agent kept retrying instead of looking for the same information inside the workspace. A timely injected prompt would have short-circuited this loop.
Proposed Behaviour
After 2 consecutive tool call denials or permission-related failures in the same session, inject a promptAsync nudge:
"Several tool calls have been denied or failed. You may be running in a sandboxed environment where file access is restricted to the project working directory. Avoid retrying the same path — find an alternative approach within the allowed workspace."
Reset the counter when the session transitions back to running (new user message detected via session.status).
Implementation
Two signals are available and should be combined:
Primary: permission.replied event
// event hook — fires after user responds to a permission dialog
if (event.type === "permission.replied" && event.properties.response === "deny") {
incrementDenyCount(event.properties.sessionID)
}
EventPermissionReplied shape (from SDK types):
{ type: "permission.replied"; properties: { sessionID: string; permissionID: string; response: string } }
Directly observes the user's deny decision. No string matching needed.
Fallback: tool.execute.after hook
"tool.execute.after": async (input, output) => {
if (/permission denied|EACCES|Operation not permitted|not allowed/i.test(output.output)) {
incrementToolErrorCount(input.sessionID)
}
}
Catches cases where the permission system is bypassed entirely (auto-deny rules, OS-level sandbox restrictions). output.output is the raw string result of the tool call.
Reset on new activity
// in the existing event handler
if (event.type === "session.status" && event.properties.status.type === "running") {
resetDenyCounts(event.properties.sessionID)
}
Threshold
Default: 2 consecutive failures before nudging. Configurable via OPENCODE_DENY_THRESHOLD env var (same pattern as OPENCODE_IDLE_THRESHOLD_MS).
Out of scope
- Auto-allowing or auto-denying permissions — the plugin must never change
output.status in permission.ask; it only observes outcomes.
- Permanent system prompt injection via
experimental.chat.system.transform — preventive hints add tokens to every request regardless of whether an issue exists; the reactive approach is preferred.
Acceptance Criteria
Summary
When the agent repeatedly hits permission denials or tool errors (e.g. trying to read outside the workspace), it often retries the same failing path instead of adapting. A nudge after 2–3 consecutive failures would remind it that it may be in a sandboxed environment and guide it toward an alternative approach.
Motivation
Observed in practice: the agent attempted to read files under
~/.cache/(not in the allowed directory list), triggered two permission dialogs, both were denied, and the agent kept retrying instead of looking for the same information inside the workspace. A timely injected prompt would have short-circuited this loop.Proposed Behaviour
After 2 consecutive tool call denials or permission-related failures in the same session, inject a
promptAsyncnudge:Reset the counter when the session transitions back to
running(new user message detected viasession.status).Implementation
Two signals are available and should be combined:
Primary:
permission.repliedeventEventPermissionRepliedshape (from SDK types):Directly observes the user's deny decision. No string matching needed.
Fallback:
tool.execute.afterhookCatches cases where the permission system is bypassed entirely (auto-deny rules, OS-level sandbox restrictions).
output.outputis the raw string result of the tool call.Reset on new activity
Threshold
Default: 2 consecutive failures before nudging. Configurable via
OPENCODE_DENY_THRESHOLDenv var (same pattern asOPENCODE_IDLE_THRESHOLD_MS).Out of scope
output.statusinpermission.ask; it only observes outcomes.experimental.chat.system.transform— preventive hints add tokens to every request regardless of whether an issue exists; the reactive approach is preferred.Acceptance Criteria
promptAsyncrunningtool.execute.aftererror pattern matching