Skip to content

Terminate approval patterns at multi-line args and summarize them in display text#1407

Merged
Aaronontheweb merged 3 commits into
netclaw-dev:devfrom
Aaronontheweb:claude-wt-command-quoting
Jun 15, 2026
Merged

Terminate approval patterns at multi-line args and summarize them in display text#1407
Aaronontheweb merged 3 commits into
netclaw-dev:devfrom
Aaronontheweb:claude-wt-command-quoting

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Fixes #1402

Problem

Multi-line quoted strings (message bodies, inline scripts) broke the shell approval flow at two layers:

  1. Stored patternsReconstructClauseText appended arg.Raw verbatim, so freshdesk ticket reply --message "Hi,\n..." persisted the full multi-line blob as the approval pattern. Every unique body became a new pattern, defeating pattern-based auto-approval and bloating the store.
  2. Display textFormatForDisplay returned the raw command including embedded newlines, which Slack/Discord/Mattermost dump into single-line code fences, corrupting the prompt layout.

Note: for #1402's exact command the stored pattern was already clean — the digit-bearing 605 terminates the walk before --message. The pattern bug only manifests when no digit-bearing token precedes the quoted arg.

Changes

All in ShellApprovalMatcher (src/Netclaw.Security/IToolApprovalMatcher.cs); no channel renderer changes needed since FormatForDisplay feeds all three plus the TUI.

Pattern path:

  • A line-break-bearing arg is now a termination condition, same mechanism as the digit-bearing value rule — the pattern stops at the preceding flag (freshdesk ticket reply --message).
  • The same rule guards redirect targets, whose quoted form survives normalization (>> "$LOGDIR\nfile" previously persisted a newline-bearing pattern).

Display path:

  • Multi-line commands are rebuilt from the parse tree: statement separators render as explicit operators, multi-line args/redirect targets become (N lines, M chars) size summaries.
  • Heredocs and subshells skip reconstruction and fall back to the flattened raw command — the parser drops heredoc bodies from the tree (a rebuild would hide the executable payload from the approver), and subshell grouping doesn't survive the flat clause list (a rebuild would misstate pipe/&& scope).
  • CompoundOperator.None on a non-first clause (emitted after a closed subshell) renders as a statement separator instead of throwing into the method's own catch.
  • Windows and parser-failure paths flatten line breaks — ShellSyntaxTree is bash-only.

Robustness:

  • Line-break detection covers lone CR as well as LF (ContainsLineBreak): a bare carriage return corrupts patterns identically and enables cursor-repositioning spoofing in terminal-rendered prompts.
  • Parse-or-fail-safe handling centralized in TryParseCommand.

Example

Command:

freshdesk ticket reply 605 --message "Hi,
We've rolled out a fix. Please verify."
Surface Before After
Display text full multi-line blob freshdesk ticket reply 605 --message (2 lines, 42 chars)
Stored pattern (no ID variant) full blob inline freshdesk ticket reply --message

Testing

  • 11 new tests in ShellApprovalMatcherTests covering pattern termination (flag/digit-ID/redirect/CR variants), display summarization, heredoc and subshell fallbacks, and statement-separator rendering — 592/592 passing
  • Edge shapes verified empirically against ShellSyntaxTree 0.1.5 (heredoc redirect targets, subshell clause flags, None-operator placement)
  • dotnet slopwatch analyze clean, copyright headers verified

Spec

openspec/specs/tool-approval-gates/spec.md amended in the same PR: line-break termination rule (LF or CR, args and redirect targets), single-line display reconstruction requirement, heredoc/subshell fallback rule, plus scenarios for each.

Deferred

Single-line quoted free-text args still inflate patterns (same class, no line break) — filed as #1406 since the fix changes grant semantics for things like git commit -m "fix" and deserves its own design discussion.

…display text (netclaw-dev#1402)

Multi-line quoted strings (message bodies, inline scripts) broke the
shell approval flow at two layers:

- Stored patterns: ReconstructClauseText appended arg.Raw verbatim, so a
  command like `freshdesk ticket reply --message "Hi,\n..."` persisted
  the full multi-line blob as the approval pattern — every unique body
  became a new pattern, defeating pattern-based auto-approval. The blob
  is now a termination condition, same mechanism as the digit-bearing
  value rule: the pattern stops at the preceding flag. The same rule
  guards redirect targets, whose quoted form survives normalization
  (`>> "$LOGDIR\nfile"` previously persisted a newline-bearing pattern).

- Display text: FormatForDisplay returned the raw command including
  embedded newlines, which Slack/Discord/Mattermost dump into
  single-line code fences. Multi-line commands are now rebuilt from the
  parse tree — statement separators render as explicit operators and
  multi-line args/redirect targets become "(N lines, M chars)" size
  summaries, with a flatten fallback on Windows or parser failure.

Line-break detection covers lone CR as well as LF: a bare carriage
return corrupts patterns the same way and enables cursor-repositioning
spoofing in terminal-rendered prompts.

Two command shapes skip display reconstruction and fall back to the
flattened raw command, because a rebuild would misrepresent what runs:
heredocs (the parser drops the body from the tree — a reconstruction
would hide the executable payload from the approver) and subshells
(grouping does not survive the flat clause list — a reconstruction
would misstate which statements a pipe or && guard applies to). The
clause after a closed subshell also carries CompoundOperator.None,
which the display walk now renders as a statement separator instead of
throwing into its own catch. Parse-or-fail-safe handling is centralized
in TryParseCommand.

Note: the digit-bearing rule already kept patterns clean when an ID
preceded the quoted arg (e.g. `... reply 605 --message "..."`); the
pattern bug only manifested without one. Spec amended accordingly.
@Aaronontheweb Aaronontheweb force-pushed the claude-wt-command-quoting branch from e0a6c35 to 9fc6b66 Compare June 12, 2026 22:09
@Aaronontheweb Aaronontheweb added the tools Issues related to agent tools: file_read, web_search, shell_execute, image processing, etc. label Jun 15, 2026
@Aaronontheweb Aaronontheweb marked this pull request as ready for review June 15, 2026 17:42

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Aaronontheweb Aaronontheweb enabled auto-merge (squash) June 15, 2026 17:43
@Aaronontheweb Aaronontheweb merged commit 0dc60f6 into netclaw-dev:dev Jun 15, 2026
15 checks passed
@Aaronontheweb Aaronontheweb deleted the claude-wt-command-quoting branch June 15, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tools Issues related to agent tools: file_read, web_search, shell_execute, image processing, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant