How the Fallback Scanner Works When Heredoc Parsing Fails in Destructive Command Guard

The fallback scanner provides a conservative rescue path that masks Ruby-style indent-stripped heredocs when the primary AST parser fails, ensuring destructive_command_guard maintains its security guarantee of zero false negatives while avoiding over-masking of ambiguous commands.

The destructive_command_guard (dcg) crate analyzes shell commands through a sophisticated three-tier pipeline to detect embedded scripts within heredocs. When the tree-sitter-bash AST parser encounters malformed input or unsupported syntax, the fallback scanner activates to handle specific edge cases—particularly the <<~ indent-stripped heredoc variant that the grammar deliberately rejects.

The Three-Tier Detection Architecture

dcg processes commands through sequential tiers to balance speed and accuracy.

Tier 1 runs a cheap trigger scanner (contains_active_heredoc_operator) that quickly determines whether a command might contain an active heredoc or inline script. If this scan returns negative, the command immediately follows the fast-path allow route.

Tier 2 attempts a full AST-based parse using tree-sitter-bash via the ast-grep library. In src/heredoc.rs, the collect_active_heredocs function constructs an AST and traverses it to identify active heredocs with precise byte offsets.

Tier 3—the fallback scanner—activates only when Tier 2 sets parse_error = true due to malformed input, missing terminators, or unsupported operators.

When the Fallback Scanner Activates

During the AST collection phase, the parser attempts to build a syntax tree:

let ast = AstGrep::new(command, SupportLang::Bash);
collect_active_heredocs(ast.root(), &mut heredocs, &mut parse_error);

When the tree-sitter grammar encounters syntax it cannot parse—such as the Ruby-style <<~ operator—it flags the error:

if parse_error {
    return active_indent_stripped_heredoc_fallback(command);
}

At this point, the normal list of active heredocs is discarded, and the fallback scanner at active_indent_stripped_heredoc_fallback in src/heredoc.rs attempts a bounded rescue.

The Five-Step Fallback Validation Process

The fallback scanner employs strict validation rules to prevent false positives while ensuring dangerous content is still masked.

Single-Operator Guard

The fallback only runs when the command contains exactly one << operator and the Tier 1 trigger scanner still believes the operator is active:

if command.match_indices("<<").count() != 1 || !contains_active_heredoc_operator(command) {
    return None;
}

This constraint prevents the fallback from making ambiguous decisions on complex commands with multiple heredocs.

Tier 2 Content Extraction

With the single-operator constraint satisfied, the fallback invokes the standard Tier 2 extractor on the entire command:

let extracted = match extract_content(command, &ExtractionLimits::default()) {
    ExtractionResult::Extracted(e) | ExtractionResult::Partial { extracted: e, .. } => e,
    _ => return None,
};

The extract_content function handles here-strings, inline-script flags, and normal heredocs without requiring a valid AST, reusing the same bounded extraction logic as the primary path.

Indent-Stripped Filtering

The fallback specifically targets the Ruby-style <<~ (indent-stripped) heredoc that tree-sitter-bash rejects. It filters the extraction results for this specific type:

let mut candidates = extracted.into_iter().filter(|c| {
    c.heredoc_type == Some(HeredocType::IndentStripped) && c.content_range.is_some()
});

This ensures the fallback only attempts to rescue heredoc types known to cause parser failures, rather than masking arbitrary malformed text.

Uniqueness Enforcement

To avoid masking potentially dangerous text in ambiguous scenarios, the fallback requires a unique candidate:

let candidate = candidates.next()?;
if candidates.next().is_some() {
    return None;
}

If multiple indent-stripped heredocs are detected, the fallback aborts and returns None, forcing the command onto the safe allow path rather than risk a false negative.

ActiveHeredoc Construction

Upon validating a single candidate, the fallback constructs a minimal ActiveHeredoc struct using the byte offsets from the extraction phase:

Some(vec![ActiveHeredoc {
    operator_start: candidate.byte_range.start,
    body: ActiveHeredocBody::Heredoc {
        body_start: body_range.start,
        body_end: body_range.end,
        delimiter_quoted: candidate.quoted,
    },
}])

This structure maintains compatibility with the rest of the masking pipeline while bypassing the failed AST parse.

Security Guarantees and Failure Modes

If any validation step fails—whether due to multiple operators, non-indent-stripped types, or ambiguous candidates—the fallback returns None. In this case, the command is treated as containing no active heredoc and follows the fast-path allow route.

This design preserves the critical security invariant of zero false negatives: the fallback only masks content when it can confidently identify a specific heredoc type known to trigger parser failures. By restricting itself to single-operator, indent-stripped heredocs, the scanner avoids over-masking legitimate commands while still catching dangerous embedded scripts that would otherwise slip through due to AST parse errors.

Implementation Reference

The fallback scanner is implemented across several key locations in the Dicklesworthstone/destructive_command_guard repository:

Practical Code Examples

Normal AST Path with Valid Bash

use destructive_command_guard::heredoc::{active_heredocs, ExtractionResult};

let cmd = "cat <<EOF\nrm -rf /tmp\nEOF";
let active = active_heredocs(cmd);
assert!(active.is_some());   // AST succeeds, heredoc detected

Fallback Rescue of Indent-Stripped Heredoc

use destructive_command_guard::heredoc::{active_heredocs, ExtractionResult};

let cmd = "cat <<~EOF\n  echo hi\nEOF"; // `<<~` is not recognized by tree-sitter-bash
let active = active_heredocs(cmd);
assert!(active.is_some());   // Fallback rescues the indent-stripped heredoc

Multiple Operators Abort Fallback

let cmd = "cat <<EOF1\nfoo\nEOF1 <<EOF2\nbar\nEOF2";
let active = active_heredocs(cmd);
assert!(active.is_none());   // More than one operator → fallback declines

No Active Heredoc Fast Path

let cmd = "git status";
let active = active_heredocs(cmd);
assert!(active.is_none());   // Tier 1 never triggered; command allowed immediately

Summary

  • The fallback scanner activates exclusively when the tree-sitter AST parser sets parse_error = true during heredoc collection.
  • It enforces a single-operator constraint, refusing to process commands containing multiple << operators to avoid ambiguity.
  • The scanner specifically targets Ruby-style indent-stripped heredocs (<<~) that the Bash grammar deliberately rejects.
  • Uniqueness validation ensures the fallback only masks content when exactly one valid candidate exists, preventing false negatives.
  • Upon failure, the fallback returns None, allowing the command to proceed through the fast-path allow route rather than risk incorrect masking.

Frequently Asked Questions

What triggers the fallback scanner in destructive_command_guard?

The fallback scanner triggers when the primary AST-based parser in collect_active_heredocs encounters malformed input or unsupported syntax—such as the Ruby <<~ operator—and sets parse_error = true. At this point, the normal heredoc list is discarded and active_indent_stripped_heredoc_fallback attempts a bounded rescue using pattern-based extraction instead of AST traversal.

Why does the fallback scanner only handle single heredoc operators?

The fallback enforces command.match_indices("<<").count() != 1 to prevent ambiguous decisions on complex commands. When multiple heredocs are present, the scanner cannot reliably determine which content to mask without the precise structural information provided by a valid AST. This constraint ensures the fallback maintains conservative security guarantees rather than risk masking the wrong content or missing dangerous scripts.

What happens if the fallback scanner fails to find a valid heredoc?

If the fallback encounters multiple candidates, non-indent-stripped types, or fails the single-operator check, it returns None. According to the implementation in src/heredoc.rs, this causes the command to be treated as containing no active heredocs, allowing it to follow the fast-path allow route. This design prioritizes availability over potentially incorrect masking, as the fallback only acts when it can confidently identify specific heredoc patterns.

Which specific heredoc syntax does the fallback target?

The fallback specifically targets indent-stripped heredocs using the <<~ operator, commonly found in Ruby and other languages. The scanner filters extraction results for HeredocType::IndentStripped because this syntax causes tree-sitter-bash to fail parsing while still representing a valid embedded script vector. The fallback does not attempt to rescue standard << heredocs or here-strings, as the AST parser handles those correctly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →