Problem
Multi-line quoted strings in shell commands break the approval flow at multiple layers. When a command like this is executed:
freshdesk ticket reply 605 --message "Hi,
We've rolled out a fix. Please verify."
The entire quoted blob — newlines included — flows through the approval system as raw unstructured text, corrupting both the displayed prompt and the stored approval patterns.
Affected Layers
1. Display text (FormatForDisplay)
Where: ShellApprovalMatcher.FormatForDisplay() → returns GetCommand(arguments) verbatim
The parser is not consulted for display. The entire command string (including multi-line quoted content) is dumped into the approval prompt.
Current behavior:
Tool: shell_execute
Request: freshdesk ticket reply 605 --message "Hi,
We've rolled out a fix. Please verify."
2. Stored approval patterns (ExtractPatterns → ReconstructClauseText)
Where: ShellApprovalMatcher.ExtractPatterns() → ReconstructClauseText(clause) iterates clause.Args and appends arg.Raw
The parser is being used here, but it re-inflates the full Arg.Raw values into stored patterns — including quoted strings with embedded newlines.
Current behavior (stored pattern):
freshdesk ticket reply --message "Hi,
We've rolled out a fix. Please verify."
Problems:
- The exact multi-line blob becomes the retry key, so a later
freshdesk ticket reply 605 with different message content won't match an "approve-once" retry
- Pattern store bloating — every unique message body becomes a new stored pattern
- Multi-line strings in the approval JSON store can break formatting/parsing
3. Approval candidate extraction (ExtractCandidates)
Where: ExtractCandidatesViaBashParser() iterates clause.Args and classifies them as paths, flags, or positional args
The quoted message arg is seen as a positional arg with multi-line content, not recognized as a display value that should be summarized.
Result: Same corruption in stored candidates and directory scoping logic.
4. Channel renderers
Where: SlackApprovalBlockBuilder, DiscordApprovalPromptBuilder, MattermostApprovalPromptBuilder
All three channels pull request.DisplayText and dump it into their prompt blocks with only size-based truncation (middle-elision), no structural parsing.
Result: The Slack Request: field contains the entire multi-line blob as a single monolithic string.
What ShellSyntaxTree Already Does Correctly
The parser is alive and working — it properly extracts:
freshdesk ticket reply as the verb chain
605 as a positional arg (later stripped by IsCallSpecificValueToken because it's digit-bearing)
--message as a flag
- The quoted string as a single
Arg with its verbatim value
But none of the rendering/display paths actually use this structure. They either bypass the parser entirely (display text) or, when they do parse, they re-inflate the full raw values instead of summarizing them.
Expected Behavior
Stored pattern (what gets saved to approval store):
Clean verb chain, no message body.
Display prompt (what user sees in Slack/Discord/Mattermost):
Tool: shell_execute
Request: freshdesk ticket reply 605 --message
└─ --message: (multi-line string, 3 lines / 142 chars)
Structured, scannable, with the actual message content summarized by size/line count rather than dumped verbatim.
Files Requiring Changes
src/Netclaw.Security/IToolApprovalMatcher.cs — ReconstructClauseText() needs to summarize/omit quoted args instead of preserving them verbatim
src/Netclaw.Security/IToolApprovalMatcher.cs — FormatForDisplay() needs to use parser output instead of raw command string
src/Netclaw.Channels.Slack/SlackApprovalBlockBuilder.cs — BuildApprovalBlocks() and BuildResolvedApprovalBlocks() should parse and structure the display text, not dump it raw
src/Netclaw.Channels.Discord/DiscordApprovalPromptBuilder.cs — same pattern
src/Netclaw.Channels.Mattermost/MattermostApprovalPromptBuilder.cs — same pattern
The ShellSyntaxTree parser is the right tool — it's already in the dependency tree, already instantiated as a singleton on ShellApprovalMatcher, and already produces structured output. The rendering/display layer just needs to consume it instead of bypassing it.
Impact
- Approval prompts are unreadable when commands contain multi-line message bodies
- Stored patterns become overly specific, preventing pattern-based auto-approval across invocations with different content
- Approve-once retries fail to match because the stored pattern includes the exact message content
- Channel renderers (especially Slack) blow past character limits with unbounded content
Problem
Multi-line quoted strings in shell commands break the approval flow at multiple layers. When a command like this is executed:
The entire quoted blob — newlines included — flows through the approval system as raw unstructured text, corrupting both the displayed prompt and the stored approval patterns.
Affected Layers
1. Display text (FormatForDisplay)
Where:
ShellApprovalMatcher.FormatForDisplay()→ returnsGetCommand(arguments)verbatimThe parser is not consulted for display. The entire command string (including multi-line quoted content) is dumped into the approval prompt.
Current behavior:
2. Stored approval patterns (ExtractPatterns → ReconstructClauseText)
Where:
ShellApprovalMatcher.ExtractPatterns()→ReconstructClauseText(clause)iteratesclause.Argsand appendsarg.RawThe parser is being used here, but it re-inflates the full
Arg.Rawvalues into stored patterns — including quoted strings with embedded newlines.Current behavior (stored pattern):
Problems:
freshdesk ticket reply 605with different message content won't match an "approve-once" retry3. Approval candidate extraction (ExtractCandidates)
Where:
ExtractCandidatesViaBashParser()iteratesclause.Argsand classifies them as paths, flags, or positional argsThe quoted message arg is seen as a positional arg with multi-line content, not recognized as a display value that should be summarized.
Result: Same corruption in stored candidates and directory scoping logic.
4. Channel renderers
Where:
SlackApprovalBlockBuilder,DiscordApprovalPromptBuilder,MattermostApprovalPromptBuilderAll three channels pull
request.DisplayTextand dump it into their prompt blocks with only size-based truncation (middle-elision), no structural parsing.Result: The Slack
Request:field contains the entire multi-line blob as a single monolithic string.What ShellSyntaxTree Already Does Correctly
The parser is alive and working — it properly extracts:
freshdesk ticket replyas the verb chain605as a positional arg (later stripped byIsCallSpecificValueTokenbecause it's digit-bearing)--messageas a flagArgwith its verbatim valueBut none of the rendering/display paths actually use this structure. They either bypass the parser entirely (display text) or, when they do parse, they re-inflate the full raw values instead of summarizing them.
Expected Behavior
Stored pattern (what gets saved to approval store):
Clean verb chain, no message body.
Display prompt (what user sees in Slack/Discord/Mattermost):
Structured, scannable, with the actual message content summarized by size/line count rather than dumped verbatim.
Files Requiring Changes
src/Netclaw.Security/IToolApprovalMatcher.cs—ReconstructClauseText()needs to summarize/omit quoted args instead of preserving them verbatimsrc/Netclaw.Security/IToolApprovalMatcher.cs—FormatForDisplay()needs to use parser output instead of raw command stringsrc/Netclaw.Channels.Slack/SlackApprovalBlockBuilder.cs—BuildApprovalBlocks()andBuildResolvedApprovalBlocks()should parse and structure the display text, not dump it rawsrc/Netclaw.Channels.Discord/DiscordApprovalPromptBuilder.cs— same patternsrc/Netclaw.Channels.Mattermost/MattermostApprovalPromptBuilder.cs— same patternThe ShellSyntaxTree parser is the right tool — it's already in the dependency tree, already instantiated as a singleton on
ShellApprovalMatcher, and already produces structured output. The rendering/display layer just needs to consume it instead of bypassing it.Impact