chore(hooks): log claude_prompt_denied on UserPromptSubmit block#2888
chore(hooks): log claude_prompt_denied on UserPromptSubmit block#2888mfbx9da4 wants to merge 3 commits into
Conversation
Adds an info log on the deny path of handleUserPromptSubmit mirroring the existing claude_hook_denied log shape used by PreToolUse, so a 403 on a claude prompt can be joined to a session in Datadog without inferring from response status alone.
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🚀 Preview Environment (PR #2888)Preview URL: https://pr-2888.dev.getgram.ai
Gram Preview Bot |
…y-prompt-deny-log # Conflicts: # server/internal/attr/conventions.go
Why
When investigating a production policy block on a Claude Code
UserPromptSubmithook, the only signal in Datadog was@http.response.status_code:403. There was no structured log event recording that the synchronous risk scan ran and matched a policy on a given session, and no easy join key from a chat row in Postgres back to server log lines. Several adjacent gaps also made it hard to debug: there was no log when the scan ran and found nothing, no log when the scan was silently skipped because the project could not be resolved, the top-level "claude hook received" log fired before plugin auth so it never carriedgram.auth.user_email, and the asynchronouspersistHookgoroutine continued the synchronous request's OTel trace so APM showed the/rpc/hooks.claudetrace polluted with detached DB writes and Temporal kickoffs.What changed (Before/After)
Before:
handleUserPromptSubmitwrote a ClickHouse block row and returned a 403, with no info-level log identifying the policy, session, or reason.scanClaudeForEnforcementemitted nothing on the success path (match or no-match).resolveClaudeScanProjectIDreturned(uuid.Nil, false)silently when there was no cached session metadata and no auth context, so failed scans were invisible."claude hook received"log fired beforewithAuthContextenriched the logger, so it never carriedgram.auth.user_emaileven on plugin-authenticated requests.recordHookkicked offpersistHookin a goroutine usingcontext.WithoutCancel(ctx), which preserved the OTel span context so the async DB writes and Temporal kickoffs hung off the synchronous request trace.After:
handleUserPromptSubmitlogsclaude_prompt_deniedwith hook source/event, session id, block reason, and matching policy id/name just before returning the 403. HTTP response shape and status are unchanged.scanClaudeForEnforcementalways logsclaude_hook_scan_decisionafter a successful scan withgram.risk.matched, plus policy id/name when matched.(uuid.Nil, false)paths inresolveClaudeScanProjectIDnow log a warnclaude_scan_no_projectwithgram.session.has_cached_metadataandgram.session.has_auth_ctxso silent skips are visible."claude hook received"info log moved to after the plugin-auth block so it carries the enriched logger (includinggram.auth.user_email) when plugin auth succeeded.persistHookgoroutine starts from a context with a cleared OTel span context, so APM no longer attributes its work to the synchronous handler trace.