fix(cli): prevent system-injected messages from appearing as user role#361
Open
hqhq1025 wants to merge 6 commits intotiann:mainfrom
Open
fix(cli): prevent system-injected messages from appearing as user role#361hqhq1025 wants to merge 6 commits intotiann:mainfrom
hqhq1025 wants to merge 6 commits intotiann:mainfrom
Conversation
…mote mode When Claude Code loads a skill, it injects the full skill content as a user message with isMeta: true. In local mode, this message passes through all existing filters (it's not a summary, not a system message) and reaches the web UI where it renders as a regular chat message. In remote mode, the OutgoingMessageQueue only filtered system-type messages. Skill injections have type 'user' with isMeta: true and slipped through. Also filter isCompactSummary messages at the source for consistency with the web normalizer, which already checks both fields. Changes: - claudeLocalLauncher: add isMeta and isCompactSummary checks - OutgoingMessageQueue: add isMeta and isCompactSummary checks - Add tests for both local launcher (6 tests) and queue (4 tests) Relates to tiann#235
…r role System-injected messages (e.g. <task-notification>, <system-reminder>) are written to the JSONL log with type:'user' but no userType:'external'. Without a userType check, sendClaudeSessionMessage classified them as role:'user', making them appear in the web UI as if the human had typed them. Fix: add userType === 'external' guard to isExternalUserMessage() so only genuine user-typed messages get role:'user'. All real user messages from both local (JSONL) and remote (SDK) paths already carry userType:'external'. Adds unit tests for isExternalUserMessage covering: normal user messages, system-injected messages, isMeta, isSidechain, tool results, and assistant messages. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
tiann
requested changes
Mar 25, 2026
Owner
tiann
left a comment
There was a problem hiding this comment.
pls fix the typecheck, thank you
Address bot review concern: adds fixture-based tests that assert every type:'user' string-content message in session JSONL fixtures carries userType:'external'. This documents and validates the assumption that isExternalUserMessage() depends on, so future regressions from upstream Claude Code changes will be caught automatically. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
The previous approach (checking `userType === 'external'`) was ineffective — Claude Code stamps ALL user messages (both real and system-injected) with `userType: 'external'`. Verified across 35 JSONL files: 1991 user messages all have the same metadata fields. The only reliable distinguisher is the message content itself. System-injected messages always start with a well-known XML tag (`<task-notification>`, `<command-name>`, `<local-command-caveat>`, `<system-reminder>`), while real user text never does. Updated isExternalUserMessage() to check content prefixes instead of metadata fields. Tests now cover all four injection tag types plus the false-positive case (user text mentioning tags mid-sentence). via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
Change return type from `boolean` to a type predicate so TypeScript can narrow the RawJSONLines union inside the true branch, allowing safe access to `body.message.content` as string. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
System-injected
type:'user'messages like<task-notification>and<system-reminder>were appearing in the HAPI web UI as if the human had typed them.Root cause
sendClaudeSessionMessageclassified a message asrole:'user'whenever:type === 'user'message.contentis a stringisSidechain !== trueisMeta !== trueClaude Code injects task notifications (background agent completions) into the conversation log as
type:'user'entries — so the model sees them in context. These messages don't haveisMeta: true, so they passed through all existing guards and landed in the UI as user messages.Fix
All genuine user-typed messages — written both by Claude Code's JSONL recorder and by
sdkToLogConverterin remote mode — carryuserType: 'external'. System-injected messages do not.Added a
userType === 'external'check, extracted into a testable helperisExternalUserMessage().Changes
cli/src/api/apiSession.ts: extractisExternalUserMessage()helper withuserType === 'external'guard; use it insendClaudeSessionMessagecli/src/api/apiSession.test.ts: 6 unit tests covering all branches ofisExternalUserMessageTest plan
claudeLocalLauncher,OutgoingMessageQueue,sessionScanner)