Skip to content

Multi-line quoted strings break shell approval prompt rendering and stored patterns #1402

@petabridge-netclaw

Description

@petabridge-netclaw

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):

freshdesk ticket reply

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

  1. src/Netclaw.Security/IToolApprovalMatcher.csReconstructClauseText() needs to summarize/omit quoted args instead of preserving them verbatim
  2. src/Netclaw.Security/IToolApprovalMatcher.csFormatForDisplay() needs to use parser output instead of raw command string
  3. src/Netclaw.Channels.Slack/SlackApprovalBlockBuilder.csBuildApprovalBlocks() and BuildResolvedApprovalBlocks() should parse and structure the display text, not dump it raw
  4. src/Netclaw.Channels.Discord/DiscordApprovalPromptBuilder.cs — same pattern
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions