Compare commits

...

1 Commits

Author SHA1 Message Date
Daniel Lok 4a9a1e2978 docs(elicitation): correct PermissionRequest tool_use_id note; drop fake id from fixtures
Claude Code's PermissionRequest hook payload carries no tool_use_id (verified against a real captured payload). The source comment called the field "not stable" rather than absent, and several test fixtures fabricated one — implying a parked prompt can be correlated to its tool call by id. It can't: there is no per-call id on PermissionRequest, so (tool_name, tool_input) is the only correlation available for the terminal-resolved fast path.

Correct the comment to say the field is absent (and why), and remove the fake tool_use_id from the PermissionRequest fixtures in both integration suites so they match the real wire shape. tool_use_ids inside tool_result transcript blocks are left untouched (those are real). No behavior change.

Co-authored-by: Isaac
2026-06-23 21:06:01 +08:00
3 changed files with 16 additions and 12 deletions
+7 -5
View File
@@ -14117,11 +14117,13 @@ def create_sessions_router(
"PermissionRequest hook body 'tool_input' must be an object when present.",
code=ErrorCode.INVALID_INPUT,
)
# ``tool_use_id`` is not stable on Claude Code's
# PermissionRequest payload, and newer builds can write the
# transcript ``function_call`` (tool_use) before this hook
# returns — so neither can correlate/resolve the parked
# request. The parked wait ends on one of three signals: an
# Claude Code's PermissionRequest payload carries no
# ``tool_use_id`` (verified against a real payload — the field
# is absent, not merely unstable; the id is only minted when the
# tool call is emitted, AFTER this permission check). And newer
# builds can write the transcript ``function_call`` (tool_use)
# before this hook returns — so neither can correlate/resolve the
# parked request. The parked wait ends on one of three signals: an
# explicit web verdict, hook disconnect, or the mirrored
# ``function_call_output`` (tool_result) for this gated tool,
# which — unlike the tool_use — is written only AFTER the
@@ -374,6 +374,9 @@ def _claude_permission_payload(tool_name: str = "Bash") -> dict[str, Any]:
:param tool_name: Tool Claude wants to call, e.g. ``"Bash"``.
:returns: JSON-serializable payload mirroring Claude Code's
published wire shape for the ``PermissionRequest`` event.
Deliberately carries no ``tool_use_id``: the real
PermissionRequest payload has no per-call id (it is minted only
when the tool call is emitted, after this permission check).
"""
return {
"session_id": "claude_sess_abc",
@@ -383,7 +386,6 @@ def _claude_permission_payload(tool_name: str = "Bash") -> dict[str, Any]:
"hook_event_name": "PermissionRequest",
"tool_name": tool_name,
"tool_input": {"command": "ls -la"},
"tool_use_id": "tool_use_xyz",
}
@@ -420,7 +422,6 @@ def _claude_ask_user_question_payload() -> dict[str, Any]:
},
],
},
"tool_use_id": "tool_use_ask",
}
@@ -72,7 +72,7 @@ async def _drain_until_elicitation(
elicitation future, so subscribing here is the simplest way to
learn the id without monkey-patching ``uuid``. Returning the
whole event lets callers also inspect the params block for the
Claude-native extras (``tool_use_id``, ``cwd``, ``permission_mode``).
Claude-native extras (``cwd``, ``permission_mode``).
:param session_id: Session to subscribe to.
:param timeout_s: Maximum seconds to wait for the elicitation
@@ -128,6 +128,9 @@ async def _claude_permission_payload(tool_name: str = "Bash") -> dict[str, Any]:
:param tool_name: Tool Claude wants to call.
:returns: JSON-serializable payload mirroring Claude Code's
published wire shape for the ``PermissionRequest`` event.
Deliberately carries no ``tool_use_id``: the real
PermissionRequest payload has no per-call id (it is minted only
when the tool call is emitted, after this permission check).
"""
return {
"session_id": "claude_sess_abc",
@@ -137,7 +140,6 @@ async def _claude_permission_payload(tool_name: str = "Bash") -> dict[str, Any]:
"hook_event_name": "PermissionRequest",
"tool_name": tool_name,
"tool_input": {"command": "ls -la"},
"tool_use_id": "tool_use_xyz",
}
@@ -440,7 +442,6 @@ async def _claude_webfetch_payload(url: str = "https://github.com/cli/cli") -> d
"hook_event_name": "PermissionRequest",
"tool_name": "WebFetch",
"tool_input": {"url": url, "prompt": "summarize"},
"tool_use_id": "tool_use_wf",
}
@@ -711,8 +712,8 @@ async def test_permission_request_hook_forwards_cwd_and_permission_mode(
``permission_mode`` lets the UI badge the card with the mode
Claude is in (``"default"`` / ``"acceptEdits"`` / ``"plan"``).
Note: ``tool_use_id`` is intentionally not asserted here.
Claude Code's PermissionRequest payload doesn't carry one
Note: ``tool_use_id`` is intentionally absent — the fixtures omit
it because Claude Code's PermissionRequest payload doesn't carry one
(the id is only minted when the tool call is emitted, AFTER
the permission check). The UI's auto-clear falls back to a
"first pending" heuristic instead — see