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

> Learn how the fallback scanner secures your commands by masking heredocs when parsing fails in destructive_command_guard, guaranteeing zero false negatives without over-masking.

- Repository: [Jeff Emanuel/destructive_command_guard](https://github.com/Dicklesworthstone/destructive_command_guard)
- Tags: internals
- Published: 2026-07-22

---

**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`](https://github.com/Dicklesworthstone/destructive_command_guard/blob/main/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:

```rust
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:

```rust
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`](https://github.com/Dicklesworthstone/destructive_command_guard/blob/main/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:

```rust
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:

```rust
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:

```rust
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**:

```rust
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:

```rust
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:

- **Trigger detection** – `check_triggers` in [[`src/heredoc.rs`](https://github.com/Dicklesworthstone/destructive_command_guard/blob/main/src/heredoc.rs)](https://github.com/Dicklesworthstone/destructive_command_guard/blob/main/src/heredoc.rs#L39-L44)
- **AST-based collection** – `collect_active_heredocs` in [[`src/heredoc.rs`](https://github.com/Dicklesworthstone/destructive_command_guard/blob/main/src/heredoc.rs)](https://github.com/Dicklesworthworthstone/destructive_command_guard/blob/main/src/heredoc.rs#L84-L94)
- **Fallback entry point** – `active_indent_stripped_heredoc_fallback` in [[`src/heredoc.rs`](https://github.com/Dicklesworthstone/destructive_command_guard/blob/main/src/heredoc.rs)](https://github.com/Dicklesworthstone/destructive_command_guard/blob/main/src/heredoc.rs#L52-L81)

## Practical Code Examples

### Normal AST Path with Valid Bash

```rust
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

```rust
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

```rust
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

```rust
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`](https://github.com/Dicklesworthstone/destructive_command_guard/blob/main/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.