Description
When a user answers a permission prompt, tool_decision reports the same result no matter what
they chose: decision: "reject", source: "", tool_name: "unknown", tool_type: "unknown".
Approvals and denials are indistinguishable, so no accept/reject ratio can be derived from
OpenCode telemetry.
handlePermissionReplied reads response from the event payload:
const { permissionID, sessionID, response } = e.properties
const pending = ctx.pendingPermissions.get(permissionID)
const decision = response === "allow" || response === "allowAlways" ? "accept" : "reject"
The field name is correct — @opencode-ai/sdk 1.18.7 declares
EventPermissionReplied.properties as { sessionID: string; permissionID: string; response: string }.
But response arrives undefined, so the strict comparison is false and the code falls through
to reject unconditionally. Separately pendingPermissions never holds the entry, so
pending?.title ?? "unknown" yields unknown — the preceding permission.updated seems not to
reach the plugin, or carries a different id.
This is not a recent regression: the same logic is unchanged from 1.2.2 through 1.5.1.
I suspect the empty response originates in the core rather than in this plugin, since the plugin
reads exactly what the SDK type declares. Neither of these core issues matches the symptom, but
they are the closest I found: anomalyco/opencode#28037 (closed — plugin permission replies
dropped via separate memoMaps) and anomalyco/opencode#34853 (open — serializing prompt settlement
around event publication). Happy to re-test a patched build, or to refile upstream if that is the
right place.
Steps to reproduce
- Configure the plugin with any OTLP collector endpoint.
- Run
opencode INTERACTIVELY. Headless opencode run auto-approves and emits no
tool_decision at all, so the bug is invisible there.
- Ask it to run a command that raises a permission prompt, e.g.
"run in bash: rm -rf /tmp/nonexistent-test-dir". In my case the prompt raised was
external_directory for /tmp/*, not the bash command itself.
- DENY the prompt.
- Repeat the same request and APPROVE the prompt.
- Inspect the collector output and compare the two
tool_decision events.
Expected behavior
The denied prompt emits decision: "reject" and the approved one emits decision: "accept",
with source carrying the actual reply (allow / allowAlways / reject) and tool_name
carrying the permission title rather than unknown.
Actual behavior
Both events are identical — the approval is indistinguishable from the denial:
{
"event.name": "tool_decision",
"decision": "reject",
"source": "",
"tool_name": "unknown",
"tool_type": "unknown",
"agent.name": "build",
"agent.type": "primary"
}
source is not an empty string: it arrives as an OTLP attribute with an empty value object,
i.e. the underlying value is undefined.
Aggregated over three weeks of real usage this yields 100% reject across every tool_decision
row, which reads as "users reject everything" unless you know it is an artifact.
Plugin version
1.5.1 (also reproduced on 1.4.0)
Configuration
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
["@devtheops/opencode-plugin-otel", {
"enabled": true,
"endpoint": "http://127.0.0.1:4317",
"protocol": "grpc",
"resourceAttributes": "service.namespace=ai-agent,repo=test",
"spanAttributes": "service.namespace=ai-agent,repo=test",
"disabledTraces": ["session", "llm", "tool"]
}]
]
}
Environment: opencode 1.18.20 (Homebrew, opencode-ai npm tarball), @opencode-ai/sdk 1.18.7
bundled in the plugin, macOS 15 arm64, collector otelcol-contrib 0.157.0 over OTLP/gRPC.
Relevant log output
Core log for the same session, showing the prompt actually being raised (note that the bash
command itself was auto-allowed and only the directory access was asked about):
level=INFO message=evaluated permission=external_directory pattern=/tmp/* action.permission=external_directory action.pattern=* action.action=ask
level=INFO message=asking id=per_051eb5da0001BZX7321ZVVAePu permission=external_directory patterns=["/tmp/*"]
level=INFO message=evaluated permission=external_directory pattern=/tmp/* action.permission=external_directory action.pattern=* action.action=ask
level=INFO message=asking id=per_051eb77580019Gz7g07LC8kOGB permission=external_directory patterns=["/tmp/*"]
level=INFO message=evaluated permission=bash pattern="rm -rf /tmp/nonexistent-test-dir" action.permission=* action.action=allow action.pattern=*
Both prompts above were answered — one denied, one approved — and both produced the identical
reject event shown in "Actual behavior".
Checklist
Description
When a user answers a permission prompt,
tool_decisionreports the same result no matter whatthey chose:
decision: "reject",source: "",tool_name: "unknown",tool_type: "unknown".Approvals and denials are indistinguishable, so no accept/reject ratio can be derived from
OpenCode telemetry.
handlePermissionRepliedreadsresponsefrom the event payload:The field name is correct —
@opencode-ai/sdk1.18.7 declaresEventPermissionReplied.propertiesas{ sessionID: string; permissionID: string; response: string }.But
responsearrivesundefined, so the strict comparison is false and the code falls throughto
rejectunconditionally. SeparatelypendingPermissionsnever holds the entry, sopending?.title ?? "unknown"yieldsunknown— the precedingpermission.updatedseems not toreach the plugin, or carries a different
id.This is not a recent regression: the same logic is unchanged from 1.2.2 through 1.5.1.
I suspect the empty
responseoriginates in the core rather than in this plugin, since the pluginreads exactly what the SDK type declares. Neither of these core issues matches the symptom, but
they are the closest I found: anomalyco/opencode#28037 (closed — plugin permission replies
dropped via separate memoMaps) and anomalyco/opencode#34853 (open — serializing prompt settlement
around event publication). Happy to re-test a patched build, or to refile upstream if that is the
right place.
Steps to reproduce
opencodeINTERACTIVELY. Headlessopencode runauto-approves and emits notool_decisionat all, so the bug is invisible there."run in bash: rm -rf /tmp/nonexistent-test-dir". In my case the prompt raised was
external_directoryfor/tmp/*, not the bash command itself.tool_decisionevents.Expected behavior
The denied prompt emits
decision: "reject"and the approved one emitsdecision: "accept",with
sourcecarrying the actual reply (allow/allowAlways/reject) andtool_namecarrying the permission title rather than
unknown.Actual behavior
Both events are identical — the approval is indistinguishable from the denial:
{ "event.name": "tool_decision", "decision": "reject", "source": "", "tool_name": "unknown", "tool_type": "unknown", "agent.name": "build", "agent.type": "primary" }sourceis not an empty string: it arrives as an OTLP attribute with an empty value object,i.e. the underlying value is
undefined.Aggregated over three weeks of real usage this yields 100%
rejectacross everytool_decisionrow, which reads as "users reject everything" unless you know it is an artifact.
Plugin version
1.5.1 (also reproduced on 1.4.0)
Configuration
{ "$schema": "https://opencode.ai/config.json", "plugin": [ ["@devtheops/opencode-plugin-otel", { "enabled": true, "endpoint": "http://127.0.0.1:4317", "protocol": "grpc", "resourceAttributes": "service.namespace=ai-agent,repo=test", "spanAttributes": "service.namespace=ai-agent,repo=test", "disabledTraces": ["session", "llm", "tool"] }] ] }Environment: opencode 1.18.20 (Homebrew,
opencode-ainpm tarball),@opencode-ai/sdk1.18.7bundled in the plugin, macOS 15 arm64, collector otelcol-contrib 0.157.0 over OTLP/gRPC.
Relevant log output
Core log for the same session, showing the prompt actually being raised (note that the bash
command itself was auto-allowed and only the directory access was asked about):
Both prompts above were answered — one denied, one approved — and both produced the identical
rejectevent shown in "Actual behavior".Checklist