Skip to content

feat(security): add InputGuard for pre-execution input screening#247

Merged
jexShain merged 1 commit into
AI-Shell-Team:mainfrom
jexShain:feature/input-guard
Jun 18, 2026
Merged

feat(security): add InputGuard for pre-execution input screening#247
jexShain merged 1 commit into
AI-Shell-Team:mainfrom
jexShain:feature/input-guard

Conversation

@jexShain

@jexShain jexShain commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add aish-security::input_guard module that screens shell commands and AI prompts before execution. Rules cover destructive commands, system compromise, resource abuse, network exfiltration, privilege escalation, and code injection, with target extraction (paths, hosts, services) so verdict messages can name the concrete risk target.
  • Wire the guard into both input paths:
    • aish-shell: AishShell::screen_input drives Allow / Confirm / Block verdicts with a raw-mode y/N prompt for Confirm. Block and user-decline record history with exit code 1 so failed attempts stay visible in history.
    • aish-pty: SessionInterceptor gains Blocked and NeedConfirm StdinAction variants so remote PTY sessions get the same screening. Tab completion is captured from PTY output and merged into the shadow line so the guard sees the fully completed command on Enter.
  • config.yaml's new input_guard_enabled mirrors security_policy.yaml's input_guard.enabled and takes precedence, giving users a single toggle for both paths. ConfigLoader now auto-migrates missing fields into existing yaml so the new key surfaces without manual editing; user values and field ordering are preserved.
  • bash_rc_wrapper.sh now strips C0 control bytes from the JSON event payload (RFC 8259 §7) and switches to LC_ALL=C for the substitution, fixing cases where stray bytes (e.g. the Ctrl-U prefix the frontend prepends) caused the Rust control-event parser to reject the whole line.

Test plan

  • cargo fmt --all -- --check (clean)
  • cargo clippy --all-targets -- -D warnings (clean)
  • cargo test --workspace (all passing: 131 + 7 + 7 + 257 + 4 + 3 + 226 + 5 + 6 + 20 + 2 + 10 + 86 + 36 + 2 = 802 tests, 0 failures)
  • Manual: typing a destructive command (e.g. rm -rf /) at the local shell prompts Block / Confirm
  • Manual: same command over a remote PTY session is intercepted with the same prompt
  • Manual: Tab-completed command (e.g. rm Tab → rm-foo) is screened on the completed form
  • Manual: setting input_guard_enabled: false in config.yaml disables screening on both paths
  • Manual: existing config.yaml without the new key gets it auto-appended on next launch without losing user values

Summary by CodeRabbit

Release Notes

  • New Features

    • Added an input guard that screens shell commands and AI prompts using configurable block/confirm rules in the security policy (enabled by default).
  • Improvements

    • Guarded PTY input now supports “blocked” behavior and interactive single-key confirmations, with safer handling for bracketed paste and improved tab-completion capture/commit.
    • Improved JSON escaping for control characters by enforcing consistent locale.
  • Bug Fixes

    • Config migration now safely inserts missing fields (including nested nulls), preserves user overrides and unknown extensions, and avoids rewriting when unchanged.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a complete InputGuard subsystem to aish-security that screens shell commands and AI prompts using regex-based Block/Confirm/Unknown/Allow verdicts. The guard is configured via InputGuardConfig in SecurityPolicy and a new input_guard_enabled field in ConfigModel. It integrates into SessionInterceptor with stdin verdict dispatch, PersistentPty with action handling and interactive confirmation, and AishShell with unified gateway flows for all command execution paths. Configuration YAML migration is also added.

Changes

InputGuard Security Screening

Layer / File(s) Summary
InputGuard core types and analysis pipeline
crates/aish-security/src/lib.rs, crates/aish-security/src/input_guard/mod.rs
Defines RuleCategory, TargetGroup, InputRule, InputVerdict, InputContext, and InputGuard; implements check pipeline with normalization, block/confirm rule application, execution-context wrapper unwrapping (ssh, eval, bash -c) with recursive re-checking, unanalyzable detection, and human-readable verdict formatting.
Block/Confirm pattern rules and tests
crates/aish-security/src/input_guard/patterns.rs
Defines cached default_rules() with Block rules (rm, dd, mkfs, fork-bomb, overwrite, chmod, disk-wipe) and Confirm rules (sudo, shell -c, eval, exec, curl upload, kill, systemctl, SSH); comprehensive test suite covers pattern matching, rule interactions, context differences, SSH recursion, target extraction, and safer-alternative population.
Target extraction and unanalyzable detection
crates/aish-security/src/input_guard/targets.rs, crates/aish-security/src/input_guard/unanalyzable.rs
Implements extract_targets with token-boundary path matching and deduplication for five TargetGroup variants; implements unanalyzable_check for remote-script-pipe, download-then-exec, heredoc, and overlong-input patterns with extracted URL targets.
InputGuardConfig and custom rule composition
crates/aish-security/src/input_guard/config.rs
Defines InputGuardConfig with enabled flag, optional max_analyzable_bytes, and custom rule lists; implements CustomRule.to_input_rule and merge_custom_rules for compiling and merging user-defined rules into block/confirm categories.
SecurityPolicy InputGuard field and YAML parsing
crates/aish-security/src/policy.rs
Adds input_guard field to SecurityPolicy with serde defaulting; updates load_policy to parse top-level input_guard YAML mapping with fallback defaults when missing or unparsable; extends policy template with input_guard example section and adds parsing validation tests.
ConfigModel field and YAML migration
crates/aish-config/Cargo.toml, crates/aish-config/src/model.rs, crates/aish-config/src/loader.rs
Adds input_guard_enabled: bool (default true) to ConfigModel; updates ConfigLoader::load to parse YAML as Value, merge missing defaults via merge_missing, deserialize before rewrite, and conditionally persist only when migration succeeds; adds tempfile dev-dependency and migration regression tests.
SessionInterceptor InputGuard integration
crates/aish-pty/src/session_interceptor.rs
Adds input_guard_enabled param to constructor and caches policy-built InputGuard per instance; adds StdinAction::Blocked and NeedConfirm variants; adds guard_shadow buffer for Enter-time screening; adds Tab-completion capture state (awaiting/pending) to prevent leakage; maps verdicts to action outcomes; adds screen_command public method; updates tests for new constructor and adds comprehensive InputGuard/completion-interaction tests.
PersistentPty InputGuard dispatch and confirmation
crates/aish-pty/src/persistent.rs
Adds input_guard_enabled param to send_command_interactive and wires it to SessionInterceptor::new; dispatches Blocked by canceling PTY-echoed line and sending Ctrl+C; dispatches NeedConfirm by reading raw y/N confirmation and either re-injecting or canceling; gates AI tool-command and BashExec injection via screen_injected_command helper; propagates &interceptor into interaction handlers; adds read_confirm_raw with ESC/Ctrl+C rejection and stdin draining.
bash_rc_wrapper control-character sanitization
crates/aish-pty/src/bash_rc_wrapper.sh
Extends __aish_json_escape with LC_ALL=C and a [:cntrl:] strip step to remove C0/DEL control bytes after JSON-escape processing.
AishShell InputGuard wiring and interactive confirmation gates
crates/aish-shell/src/app.rs, config/security_policy.yaml
Adds InputGuard field to AishShell initialized from policy (overridden by config.input_guard_enabled); adds confirm_action/screen_input unified verdict handlers with raw-mode [y/N] prompt for Confirm/Unknown; gates AI execution, NL-detected AI routing, PTY builtins, error-corrected approvals, slash-popup dismissals, and script calls; propagates input_guard_enabled to PTY calls; adds ConfirmResponse, interpret_confirm_byte, keystroke reading, and stdin draining helpers with tests; updates security policy template with input_guard configuration section and guidance.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AishShell
  participant InputGuard
  participant PersistentPty

  rect rgba(70, 130, 180, 0.5)
    note over AishShell,InputGuard: Non-PTY command path
    User->>AishShell: enter command
    AishShell->>InputGuard: check(input, ShellCommand)
    alt Block
      InputGuard-->>AishShell: Block{reason}
      AishShell->>User: print reason in red, record history exit=1
    else Confirm/Unknown
      InputGuard-->>AishShell: Confirm{reason}/Unknown{reason}
      AishShell->>User: yellow warning + raw [y/N] keystroke
      alt confirmed
        User-->>AishShell: y/Y
        AishShell->>PersistentPty: send_command_interactive(input_guard_enabled=true)
      else declined
        User-->>AishShell: n/N/Ctrl+C/ESC
        AishShell->>User: print Block reason in red
        AishShell->>AishShell: record history exit=1
      end
    else Allow
      InputGuard-->>AishShell: Allow
      AishShell->>PersistentPty: send_command_interactive(input_guard_enabled=true)
    end
  end

  rect rgba(100, 180, 100, 0.5)
    note over PersistentPty,InputGuard: Interactive PTY session path (session interceptor)
    PersistentPty->>PersistentPty: SessionInterceptor::new(..., input_guard_enabled)
    User->>PersistentPty: keystroke → feed_stdin(Enter)
    PersistentPty->>InputGuard: check(guard_shadow, ShellCommand)
    alt Blocked
      InputGuard-->>PersistentPty: Block{reason}
      PersistentPty->>PersistentPty: send Ctrl+C, display reason
    else NeedConfirm
      InputGuard-->>PersistentPty: Confirm{reason,line}
      PersistentPty->>User: read_confirm_raw [y/N]
      alt confirmed
        User-->>PersistentPty: y/Y
        PersistentPty->>PersistentPty: re-inject line + \r
      else declined
        User-->>PersistentPty: n/N/Ctrl+C/ESC
        PersistentPty->>PersistentPty: send Ctrl+C, print Cancelled
      end
    else Allow
      InputGuard-->>PersistentPty: Allow
      PersistentPty->>PersistentPty: forward command
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • AI-Shell-Team/aish#154: Both PRs modify crates/aish-security/src/policy.rs for YAML-based security policy loading and extend the SecurityPolicy data model.
  • AI-Shell-Team/aish#196: Both PRs add approval/confirmation gating to the AI/PTY execution flow via PersistentPty::send_command_interactive and aish-shell/src/app.rs (this PR: InputGuard screening; referenced PR: secret-detection confirmation).
  • AI-Shell-Team/aish#125: Both PRs modify crates/aish-pty/src/persistent.rs and PersistentPty::send_command_interactive signature, extending the session-command execution flow with new safety gates.

Suggested labels

security, config, tests, docs

Poem

🐇 Hop hop, the rabbit guards the gate,
With Block and Confirm to keep commands straight,
rm -rf /? Not on my watch today!
A [y/N] prompt keeps the danger at bay,
Config migrated, YAML gets a facelift—
Safety merged in, what a wonderful gift! 🛡️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: introducing InputGuard for input screening before execution.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the pull request. A maintainer will review it when available.

Please keep the PR focused, explain the why in the description, and make sure local checks pass before requesting review.

Contribution guide: https://github.com/AI-Shell-Team/aish/blob/main/CONTRIBUTING.md

@github-actions

Copy link
Copy Markdown
Contributor

This pull request description looks incomplete. Please update the missing sections below before review.

Missing items:

  • User-visible Changes
  • Compatibility
  • Testing
  • Change Type
  • Scope

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/aish-shell/src/app.rs (2)

1748-1749: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Screen AI-corrected commands before execution.

The error-correction flow executes the AI-suggested corrected command directly after a generic Y/n prompt, bypassing screen_shell_command, so a destructive corrected command can skip the new InputGuard gate.

Proposed fix
                                             if answer == "y" || answer == "yes" || answer.is_empty()
                                             {
-                                                let exit_code =
-                                                    self.execute_external_command(corrected);
-                                                self.record_history(corrected, exit_code);
+                                                if self.screen_shell_command(corrected) {
+                                                    let exit_code =
+                                                        self.execute_external_command(corrected);
+                                                    self.record_history(corrected, exit_code);
+                                                }
                                             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-shell/src/app.rs` around lines 1748 - 1749, The AI-corrected
command is being executed directly without screening, which bypasses the
InputGuard protection gate. In the error-correction flow where
`self.execute_external_command(corrected)` is called, the `corrected` command
needs to be screened through the `screen_shell_command` method first (just like
other commands are) to ensure it passes the InputGuard validation before being
executed. This ensures that destructive corrected commands are caught by the
same safety gate as other commands.

2297-2351: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply InputGuard to slash-dismissed submissions.

read_line_after_slash_dismiss routes submitted text through process_readline_submission, which executes commands/scripts without screen_shell_command. Typing /, dismissing the popup, then submitting a destructive command bypasses the new guard.

Proposed fix direction
             crate::types::InputIntent::Command | crate::types::InputIntent::OperatorCommand => {
+                if !self.screen_shell_command(input) {
+                    return false;
+                }
                 self.set_phase(ShellPhase::Running);
                 let exit_code = self.execute_external_command(input);
@@
             crate::types::InputIntent::ScriptCall => {
+                if !self.screen_shell_command(input) {
+                    return false;
+                }
                 let exit_code = self.execute_script(input);
                 self.record_history(input, exit_code);
                 false

Also mirror the main-loop route_to_pty handling for builtins such as sudo/su, or preferably delegate both paths to one shared submission handler.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-shell/src/app.rs` around lines 2297 - 2351, The method
`process_readline_submission` executes commands and scripts without applying the
InputGuard protection (via `screen_shell_command`), allowing destructive
commands to bypass security checks when a user dismisses the slash popup and
submits. Apply InputGuard protection to both the `execute_external_command` call
in the Command/OperatorCommand branch and the `execute_script` call in the
ScriptCall branch by routing them through the same guard mechanism used in the
main loop, or consolidate both submission paths into a single shared handler
that applies the guard consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aish-config/src/loader.rs`:
- Around line 64-66: The current implementation writes the migrated config
directly to the target file using std::fs::write, which can result in a
corrupted config.yaml if the process crashes during the write operation. Instead
of writing directly to the path, create a temporary file in the same directory
as path, write the migrated content to this temporary file, and then use
std::fs::rename to atomically replace the original file. This ensures that the
target file remains intact if the write operation is interrupted.
- Around line 61-66: The current migration rewrite serializes only the typed
ConfigModel using serde_yaml::to_string(&config), which loses any unknown keys
and user extensions from the original YAML. Instead of re-serializing the typed
model, preserve the merged serde_yaml::Value after successfully deserializing it
to the typed model. Replace the to_string call in the migration block with
writing the preserved Value directly to ensure the migration is additive rather
than lossy and maintains all original content while applying the necessary
updates.

In `@crates/aish-pty/src/persistent.rs`:
- Around line 1091-1122: In the StdinAction::Blocked(reason) branch, after
displaying the block reason, clear the stdin_shadow buffer to keep it in sync.
In the StdinAction::NeedConfirm { reason, line } branch, when the user confirms
(confirmed is true), execute the same nested-session update logic that would run
in the Forward branch for the line parameter (updating remote_host_for_probe and
shared_host) before clearing stdin_shadow. When the user declines or in the
blocked case, also clear stdin_shadow to prevent stale data from persisting.
- Around line 453-455: The InputGuard is currently only applied in the
feed_stdin method, but AI-generated commands injected through response.command
and BashExec paths bypass this guard before being written to master_fd. Locate
all command injection sites in SessionInterceptor where AI responses or BashExec
commands are written to master_fd (at the specified line ranges 927-967,
2070-2103, and 3541-3563), and apply the same InputGuard logic that exists in
feed_stdin before these writes occur. Gate each injection point with the
InputGuard to ensure all shell commands, whether from user input or AI
responses, are screened and verified before execution.
- Around line 3019-3054: The read_confirm_raw function reads a confirmation byte
but does not drain remaining input from stdin after making a decision, which can
cause trailing bytes like Enter to be forwarded unintentionally to the remote
session. After each match arm in the match statement (the ones handling
Some(b'y'|b'Y'), Some(0x03), Some(0x1b), Some(_), and None), add a call to drain
any remaining bytes from stdin before returning the boolean decision. This
ensures that only the user's intentional confirmation byte is consumed and any
accidental trailing input is cleared.
- Around line 1095-1117: The issue is that in both the Blocked and NeedConfirm
branches, the Ctrl+C byte (0x03) is queued to write_buf via
interceptor.take_cancel_pty_line() but the buffer is not flushed before blocking
on show() or read_confirm_raw(). This causes the pending cancel byte to be sent
after user confirmation, interfering with the reinjected line. Fix this by
ensuring the write_buf containing the Ctrl+C is flushed to the PTY immediately
after pushing 0x03 and before calling show() in the Blocked branch or
read_confirm_raw() in the NeedConfirm branch, so the cancel is actually sent to
the remote shell before the user interaction or line reinjection occurs.

In `@crates/aish-pty/src/session_interceptor.rs`:
- Around line 289-296: The InputGuard screening in the bracketed paste handling
is checking only `line_shadow` which excludes pasted bytes, allowing commands to
bypass security checks when pasted. To fix this, modify the code around the
`self.input_guard.check()` call to construct the complete command line that
includes both the existing `line_shadow` content and any pasted bytes before
passing it to the guard. Either maintain a separate shadow buffer for the guard
that tracks all input including pastes, or reconstruct the full line by
combining line_shadow with the buffered paste content when creating the string
passed to `self.input_guard.check()`. Ensure that while the guard sees the
complete command for security evaluation, AI and status message triggers are
still suppressed separately for pasted newlines. Apply the same fix at the
second location noted in the comment.

In `@crates/aish-shell/src/app.rs`:
- Around line 2074-2077: The current code path calls screen_shell_command(input)
before routing input through natural language processing, which causes NL-like
inputs such as "how do I kill a process?" to be screened as shell commands first
and trigger shell confirmation prematurely. Reorder the logic in the input
handling section around screen_shell_command so that natural language routing is
evaluated first, allowing the InputGuard to properly route NL prompts to AI
context (which intentionally skips Confirm rules) before any shell command
screening occurs.
- Around line 402-407: The read_confirm_keystroke function consumes only a
single byte from stdin, leaving remaining input (such as the Enter key or
additional characters when a user types "yes<Enter>") queued in the input
buffer. These leftover bytes will be consumed by the next prompt as unintended
input. After calling read_confirm_keystroke(stdin_fd), drain the remaining bytes
from the stdin buffer before restoring the terminal settings with tcsetattr to
ensure no trailing input interferes with subsequent prompts. This same issue
also needs to be fixed in the other locations mentioned in the comment (lines
5382-5399).
- Around line 5390-5391: Replace the unsafe errno access pattern using
`libc::__errno_location()` at lines 5390-5391 with the portable standard library
approach. Instead of directly accessing errno through libc, use
`std::io::Error::last_os_error().raw_os_error()` which returns an `Option<i32>`.
Update the conditional check to compare against the unwrapped value and verify
it matches `libc::EINTR` to maintain the same logic while using the more
portable and consistent approach already used elsewhere in the codebase such as
in `crates/aish-pty/src/persistent.rs`.

---

Outside diff comments:
In `@crates/aish-shell/src/app.rs`:
- Around line 1748-1749: The AI-corrected command is being executed directly
without screening, which bypasses the InputGuard protection gate. In the
error-correction flow where `self.execute_external_command(corrected)` is
called, the `corrected` command needs to be screened through the
`screen_shell_command` method first (just like other commands are) to ensure it
passes the InputGuard validation before being executed. This ensures that
destructive corrected commands are caught by the same safety gate as other
commands.
- Around line 2297-2351: The method `process_readline_submission` executes
commands and scripts without applying the InputGuard protection (via
`screen_shell_command`), allowing destructive commands to bypass security checks
when a user dismisses the slash popup and submits. Apply InputGuard protection
to both the `execute_external_command` call in the Command/OperatorCommand
branch and the `execute_script` call in the ScriptCall branch by routing them
through the same guard mechanism used in the main loop, or consolidate both
submission paths into a single shared handler that applies the guard
consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e817528-3dd5-4bcd-80b0-6c33fdf3e394

📥 Commits

Reviewing files that changed from the base of the PR and between 636e551 and a928d32.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • crates/aish-config/Cargo.toml
  • crates/aish-config/src/loader.rs
  • crates/aish-config/src/model.rs
  • crates/aish-pty/src/bash_rc_wrapper.sh
  • crates/aish-pty/src/persistent.rs
  • crates/aish-pty/src/session_interceptor.rs
  • crates/aish-security/src/input_guard/config.rs
  • crates/aish-security/src/input_guard/mod.rs
  • crates/aish-security/src/input_guard/patterns.rs
  • crates/aish-security/src/input_guard/targets.rs
  • crates/aish-security/src/input_guard/unanalyzable.rs
  • crates/aish-security/src/lib.rs
  • crates/aish-security/src/policy.rs
  • crates/aish-shell/src/app.rs

Comment thread crates/aish-config/src/loader.rs Outdated
Comment thread crates/aish-config/src/loader.rs Outdated
Comment thread crates/aish-pty/src/persistent.rs
Comment thread crates/aish-pty/src/persistent.rs
Comment thread crates/aish-pty/src/persistent.rs
Comment thread crates/aish-pty/src/persistent.rs Outdated
Comment thread crates/aish-shell/src/app.rs
Comment thread crates/aish-shell/src/app.rs Outdated
Comment thread crates/aish-shell/src/app.rs Outdated
@jexShain jexShain force-pushed the feature/input-guard branch from a928d32 to 28f8c8c Compare June 17, 2026 10:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/aish-pty/src/session_interceptor.rs (1)

290-297: ⚠️ Potential issue | 🔴 Critical

Add InputGuard screening before invoking AI callback in persistent.rs SSH handler.

Remote SSH prompts bypass InputGuard screening. The TriggerAi handler in persistent.rs (line 1364+) invokes call_ai directly without screening the question through InputContext::AiPrompt. In contrast, local shell (app.rs) calls screen_ai_prompt() before AI processing. Remote users can thus inject AI prompts containing dangerous patterns (e.g., ;rm -rf /) that are not validated by the InputGuard.

Call screen_ai_prompt(question) or equivalent before line 1587's interceptor.call_ai(question, ...) in the SSH session handler to match the local shell's security posture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-pty/src/session_interceptor.rs` around lines 290 - 297, The SSH
session handler in persistent.rs is invoking interceptor.call_ai without
screening the AI question through InputGuard validation, creating a security
vulnerability where dangerous patterns can bypass input validation. Before the
call to interceptor.call_ai(question, ...) at line 1587 in persistent.rs, add a
call to screen_ai_prompt(question) to validate the question through
InputContext::AiPrompt, matching the security validation performed by the local
shell handler in app.rs. This ensures remote SSH prompts receive the same
InputGuard screening as local shell input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aish-pty/src/persistent.rs`:
- Around line 1113-1209: The issue is that after a guard decision (Blocked or
NeedConfirm), the for &byte in data loop continues processing remaining bytes
from the current stdin batch instead of discarding them, which can leak extra
input after the guarded command. Fix this by adding a break statement at the end
of both the crate::StdinAction::Blocked(reason) and
crate::StdinAction::NeedConfirm { reason, line } match arms (after setting
skip_leading_newline in each case) to exit the loop and discard any remaining
bytes in the current data batch.

In `@crates/aish-pty/src/session_interceptor.rs`:
- Around line 258-260: The issue is that the code commits pending completion
when a non-tab byte arrives without waiting for actual PTY completion
confirmation, creating a security vulnerability where unconfirmed completion
state allows stale buffer content to be forwarded. Remove the call to
commit_pending_completion() in the condition checking self.awaiting_completion
&& byte != 0x09, and instead implement an "uncertain completion" state that
blocks line forwarding until the actual PTY completion response is received.
This same issue also affects the related code around lines 472-477, so both
locations need to be updated to treat unresolved completions as untrusted and
require explicit verification before allowing commands to proceed.
- Around line 299-308: The issue is that handle_escape_seq_byte forwards certain
CSI sequences without updating guard_shadow, causing desynchronization between
the shadow buffer and the actual terminal content. When users recall commands
using arrow keys and press Enter, the input guard checks against stale shadow
data instead of the actual command. To fix this: mark guard_shadow as untrusted
or invalidated whenever handle_escape_seq_byte forwards unsupported readline or
history editing sequences, then at the location where the line is extracted from
guard_shadow using String::from_utf8_lossy, add logic to detect and handle
desynchronized state by either blocking the command, requesting user
confirmation, or reconstructing the actual line before passing it to
input_guard.check. This same pattern should be applied to all locations
mentioned in the review including the ranges around lines 299-308 and 398-443.
- Around line 318-325: The saved variable is being assigned from line_shadow,
which excludes bracketed-paste bytes, but the NeedConfirm action uses this saved
value for reinjection after user approval. For a pasted command like sudo ls,
this results in reinjecting an empty or truncated line. Instead of cloning
line_shadow, clone guard_shadow which is the screened buffer that preserves all
necessary content including bracketed-paste information, ensuring the caller
reinjects the complete command after approval.

In `@crates/aish-security/src/input_guard/config.rs`:
- Around line 45-47: The to_input_rule method is silently discarding regex
compilation errors by using .ok(), which means misconfigured rule patterns are
dropped without any warning or validation feedback. Instead of using .ok() to
suppress the error, modify the to_input_rule method to return a Result type or
an error type that communicates what went wrong with the regex pattern. Then
update the merge_custom_rules method (which calls to_input_rule) to collect and
report the invalid rule names and their associated errors instead of silently
skipping them, ensuring that configuration mistakes fail visibly during policy
validation or through logging.

---

Outside diff comments:
In `@crates/aish-pty/src/session_interceptor.rs`:
- Around line 290-297: The SSH session handler in persistent.rs is invoking
interceptor.call_ai without screening the AI question through InputGuard
validation, creating a security vulnerability where dangerous patterns can
bypass input validation. Before the call to interceptor.call_ai(question, ...)
at line 1587 in persistent.rs, add a call to screen_ai_prompt(question) to
validate the question through InputContext::AiPrompt, matching the security
validation performed by the local shell handler in app.rs. This ensures remote
SSH prompts receive the same InputGuard screening as local shell input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70472fee-e500-41ab-afd8-9adfda13ba2e

📥 Commits

Reviewing files that changed from the base of the PR and between a928d32 and 28f8c8c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • crates/aish-config/Cargo.toml
  • crates/aish-config/src/loader.rs
  • crates/aish-config/src/model.rs
  • crates/aish-pty/src/bash_rc_wrapper.sh
  • crates/aish-pty/src/persistent.rs
  • crates/aish-pty/src/session_interceptor.rs
  • crates/aish-security/src/input_guard/config.rs
  • crates/aish-security/src/input_guard/mod.rs
  • crates/aish-security/src/input_guard/patterns.rs
  • crates/aish-security/src/input_guard/targets.rs
  • crates/aish-security/src/input_guard/unanalyzable.rs
  • crates/aish-security/src/lib.rs
  • crates/aish-security/src/policy.rs
  • crates/aish-shell/src/app.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/aish-config/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (10)
  • crates/aish-security/src/lib.rs
  • crates/aish-pty/src/bash_rc_wrapper.sh
  • crates/aish-config/src/model.rs
  • crates/aish-security/src/input_guard/targets.rs
  • crates/aish-security/src/policy.rs
  • crates/aish-config/src/loader.rs
  • crates/aish-security/src/input_guard/patterns.rs
  • crates/aish-security/src/input_guard/unanalyzable.rs
  • crates/aish-security/src/input_guard/mod.rs
  • crates/aish-shell/src/app.rs

Comment thread crates/aish-pty/src/persistent.rs
Comment thread crates/aish-pty/src/session_interceptor.rs
Comment thread crates/aish-pty/src/session_interceptor.rs
Comment thread crates/aish-pty/src/session_interceptor.rs Outdated
Comment thread crates/aish-security/src/input_guard/config.rs
@jexShain jexShain force-pushed the feature/input-guard branch from 28f8c8c to 67ff326 Compare June 18, 2026 03:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/aish-security/src/input_guard/targets.rs (1)

21-26: 💤 Low value

Consider adding additional block device patterns for completeness.

The current regex covers common device types but misses some environments:

  • /dev/xvd* (Xen virtual devices, common in AWS)
  • /dev/loop* (loop devices)
  • /dev/dm-* (device mapper, LVM)
  • /dev/md* (software RAID)
♻️ Suggested enhancement
     RE.get_or_init(|| {
-        Regex::new(r"/dev/(?:sd[a-z]\d*|nvme\d+n\d+(?:p\d+)?|vd[a-z]\d*|hd[a-z]\d*)").unwrap()
+        Regex::new(r"/dev/(?:sd[a-z]\d*|nvme\d+n\d+(?:p\d+)?|vd[a-z]\d*|hd[a-z]\d*|xvd[a-z]\d*|loop\d+|dm-\d+|md\d+)").unwrap()
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-security/src/input_guard/targets.rs` around lines 21 - 26, The
block_device_re() function's regex pattern is incomplete and misses several
important block device types used in different environments. Update the regex
pattern in the Regex::new() call to include additional device patterns for Xen
virtual devices (xvd*), loop devices (loop*), device mapper/LVM devices (dm-*),
and software RAID devices (md*) alongside the existing patterns for SATA/SCSI,
NVMe, KVM virtual, and IDE devices. Ensure the pattern properly handles the
specific naming conventions and optional numbering for each device type.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/aish-security/src/input_guard/targets.rs`:
- Around line 21-26: The block_device_re() function's regex pattern is
incomplete and misses several important block device types used in different
environments. Update the regex pattern in the Regex::new() call to include
additional device patterns for Xen virtual devices (xvd*), loop devices (loop*),
device mapper/LVM devices (dm-*), and software RAID devices (md*) alongside the
existing patterns for SATA/SCSI, NVMe, KVM virtual, and IDE devices. Ensure the
pattern properly handles the specific naming conventions and optional numbering
for each device type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3baead33-658c-444f-b2b6-5945c27e8e4b

📥 Commits

Reviewing files that changed from the base of the PR and between 28f8c8c and 67ff326.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • crates/aish-config/Cargo.toml
  • crates/aish-config/src/loader.rs
  • crates/aish-config/src/model.rs
  • crates/aish-pty/src/bash_rc_wrapper.sh
  • crates/aish-pty/src/persistent.rs
  • crates/aish-pty/src/session_interceptor.rs
  • crates/aish-security/src/input_guard/config.rs
  • crates/aish-security/src/input_guard/mod.rs
  • crates/aish-security/src/input_guard/patterns.rs
  • crates/aish-security/src/input_guard/targets.rs
  • crates/aish-security/src/input_guard/unanalyzable.rs
  • crates/aish-security/src/lib.rs
  • crates/aish-security/src/policy.rs
  • crates/aish-shell/src/app.rs
🚧 Files skipped from review as they are similar to previous changes (11)
  • crates/aish-security/src/lib.rs
  • crates/aish-config/Cargo.toml
  • crates/aish-pty/src/bash_rc_wrapper.sh
  • crates/aish-security/src/input_guard/config.rs
  • crates/aish-security/src/policy.rs
  • crates/aish-security/src/input_guard/unanalyzable.rs
  • crates/aish-security/src/input_guard/mod.rs
  • crates/aish-security/src/input_guard/patterns.rs
  • crates/aish-pty/src/persistent.rs
  • crates/aish-pty/src/session_interceptor.rs
  • crates/aish-shell/src/app.rs

@jexShain jexShain force-pushed the feature/input-guard branch 2 times, most recently from 3da65eb to 26e5446 Compare June 18, 2026 06:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/aish-shell/src/app.rs (2)

2212-2214: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate main-loop script invocations as well.

The slash-dismissed path now screens ScriptCall, but the main REPL branch still executes scripts directly. If script parsing fails, execute_script falls back to execute_external_command(input), so the invocation itself should clear the same shell-command gate.

Proposed fix
                 crate::types::InputIntent::ScriptCall => {
+                    if !self.screen_shell_command(input) {
+                        continue;
+                    }
                     let exit_code = self.execute_script(input);
                     self.record_history(input, exit_code);
                 }

Also applies to: 2377-2383

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-shell/src/app.rs` around lines 2212 - 2214, The main REPL branch
handling ScriptCall in the InputIntent match statement (around lines 2212-2214)
and the similar code at lines 2377-2383 should apply the same screening gate
that was applied to the slash-dismissed path. Before calling
self.execute_script(input), wrap the script execution with the same conditional
check that validates the invocation against the shell-command gate, ensuring
consistent gating behavior across all script invocation paths in the main loop.

2020-2074: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Screen state-modifying builtins before mutating shell state.

handle_builtin runs before the InputGuard check, so a blocked/declined cd/export/unset can already change Rust-side state. Also, screen_shell_command records exit code 1, then Line 2071 records the same rejected input as success.

Proposed fix
                     let parts: Vec<&str> = input.split_whitespace().collect();
                     if let Some(cmd) = parts.first() {
+                        let state_modifying = crate::commands::is_state_modifying(cmd)
+                            && !crate::commands::is_rejected(cmd);
+                        if state_modifying && !self.screen_shell_command(input) {
+                            continue;
+                        }
+
                         let result = self.state.handle_builtin(cmd, &parts[1..]);
@@
-                        if crate::commands::is_state_modifying(cmd)
-                            && !crate::commands::is_rejected(cmd)
-                        {
-                            if !self.screen_shell_command(input) {
-                                // Destructive payload blocked — skip the sync
-                                // so bash never sees it. State in Rust is
-                                // already updated by handle_builtin above,
-                                // but the destructive payload in the value
-                                // never reaches bash.
-                                self.record_history(input, 0);
-                                continue;
-                            }
+                        if state_modifying {
                             self.sync_command_to_pty(input);
                         }
@@
-                        if crate::commands::is_state_modifying(cmd)
-                            && !crate::commands::is_rejected(cmd)
-                        {
+                        if state_modifying {
                             entry.push_str(&format!("\n<cwd>{}</cwd>", self.state.cwd));
                         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-shell/src/app.rs` around lines 2020 - 2074, The issue is that
handle_builtin is called before InputGuard screening for state-modifying
commands, allowing rejected commands to mutate shell state, and
screen_shell_command records exit code 1 while record_history then records the
same input with exit code 0. Move the InputGuard check for state-modifying
commands (the is_state_modifying and is_rejected condition block) to execute
BEFORE calling handle_builtin so rejected commands never reach handle_builtin
and don't mutate state. Remove the redundant record_history call inside the
blocked command branch to avoid double-recording since screen_shell_command
already handles recording the rejection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aish-shell/src/app.rs`:
- Around line 3263-3283: The pre-screen logic in the script screening loop uses
a simple prefix check (starts_with "ai ") to skip lines, but this is
inconsistent with how the actual execution handler validates AI calls using a
regex pattern (ai_call_re). This allows malformed lines like `ai $(rm -rf /)` to
incorrectly bypass shell command screening. Replace the simple prefix checks in
the loop with the same regex pattern validation used by the actual execution
handler to ensure only properly formatted AI calls matching ai_call_re are
skipped from shell screening, and ensure all AI prompts are properly validated
through screen_ai_prompt before being processed.

---

Outside diff comments:
In `@crates/aish-shell/src/app.rs`:
- Around line 2212-2214: The main REPL branch handling ScriptCall in the
InputIntent match statement (around lines 2212-2214) and the similar code at
lines 2377-2383 should apply the same screening gate that was applied to the
slash-dismissed path. Before calling self.execute_script(input), wrap the script
execution with the same conditional check that validates the invocation against
the shell-command gate, ensuring consistent gating behavior across all script
invocation paths in the main loop.
- Around line 2020-2074: The issue is that handle_builtin is called before
InputGuard screening for state-modifying commands, allowing rejected commands to
mutate shell state, and screen_shell_command records exit code 1 while
record_history then records the same input with exit code 0. Move the InputGuard
check for state-modifying commands (the is_state_modifying and is_rejected
condition block) to execute BEFORE calling handle_builtin so rejected commands
never reach handle_builtin and don't mutate state. Remove the redundant
record_history call inside the blocked command branch to avoid double-recording
since screen_shell_command already handles recording the rejection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a170eab-57a0-4fc1-a6e7-a64ec1abde43

📥 Commits

Reviewing files that changed from the base of the PR and between 3da65eb and 26e5446.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • config/security_policy.yaml
  • crates/aish-config/Cargo.toml
  • crates/aish-config/src/loader.rs
  • crates/aish-config/src/model.rs
  • crates/aish-pty/src/bash_rc_wrapper.sh
  • crates/aish-pty/src/persistent.rs
  • crates/aish-pty/src/session_interceptor.rs
  • crates/aish-security/src/input_guard/config.rs
  • crates/aish-security/src/input_guard/mod.rs
  • crates/aish-security/src/input_guard/patterns.rs
  • crates/aish-security/src/input_guard/targets.rs
  • crates/aish-security/src/input_guard/unanalyzable.rs
  • crates/aish-security/src/lib.rs
  • crates/aish-security/src/policy.rs
  • crates/aish-shell/src/app.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/aish-config/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (13)
  • crates/aish-security/src/lib.rs
  • crates/aish-config/src/model.rs
  • config/security_policy.yaml
  • crates/aish-pty/src/bash_rc_wrapper.sh
  • crates/aish-security/src/policy.rs
  • crates/aish-security/src/input_guard/config.rs
  • crates/aish-security/src/input_guard/unanalyzable.rs
  • crates/aish-config/src/loader.rs
  • crates/aish-security/src/input_guard/targets.rs
  • crates/aish-security/src/input_guard/patterns.rs
  • crates/aish-pty/src/persistent.rs
  • crates/aish-security/src/input_guard/mod.rs
  • crates/aish-pty/src/session_interceptor.rs

Comment thread crates/aish-shell/src/app.rs
@jexShain jexShain force-pushed the feature/input-guard branch 2 times, most recently from c9dfc27 to 4596bf4 Compare June 18, 2026 08:15
Add aish-security::input_guard module that screens shell commands and
AI prompts before execution. Rules cover destructive commands, system
compromise, resource abuse, network exfiltration, privilege escalation,
and code injection, with target extraction for paths, hosts, and
services so messages can name the concrete risk target.

Wire the guard into both input paths:

- aish-shell: AishShell::screen_input drives verdicts to Allow / Confirm
  / Block, with a raw-mode y/N prompt for Confirm. Block and user-decline
  record history with exit code 1 so failed attempts stay visible.
- aish-pty: SessionInterceptor gains Blocked and NeedConfirm StdinAction
  variants so remote PTY sessions get the same screening. Tab completion
  is captured from PTY output and merged into the shadow line so the
  guard sees the fully completed command on Enter.

config.yaml's new input_guard_enabled mirrors
security_policy.yaml's input_guard.enabled and takes precedence, giving
users a single toggle for both paths. ConfigLoader now auto-migrates
missing fields into existing yaml so the new key surfaces without
manual editing; user values and ordering are preserved.

bash_rc_wrapper.sh now strips C0 control bytes from the JSON event
payload (RFC 8259 §7) and switches to LC_ALL=C for the substitution,
fixing cases where stray bytes (e.g. Ctrl-U prefix from the frontend)
caused the Rust control-event parser to reject the whole line.
@jexShain jexShain force-pushed the feature/input-guard branch from 4596bf4 to 0a69fe7 Compare June 18, 2026 08:54
@jexShain jexShain merged commit 0312ca5 into AI-Shell-Team:main Jun 18, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant