Skip to content

fix: segment-aware snip detection + quiet mode + shell keywords (closes #15, #16)#19

Closed
EnRaiha wants to merge 3 commits into
VincentHardouin:mainfrom
EnRaiha:main
Closed

fix: segment-aware snip detection + quiet mode + shell keywords (closes #15, #16)#19
EnRaiha wants to merge 3 commits into
VincentHardouin:mainfrom
EnRaiha:main

Conversation

@EnRaiha
Copy link
Copy Markdown

@EnRaiha EnRaiha commented Jun 2, 2026

Fixes #15 (snowball) + #16 (quiet mode) + shell keyword protection.

Prevention over Reaction

PR #17 deduplicates snip snip snip after it happens. This PR prevents it entirely with segment-aware detection.

Changes

Fix #15 — Snowball bug

  • alreadyWrapped() scans compound command segments (&&, ||, ;, &) for existing snip prefix
  • Handles env var prefixes: FOO=bar snip cmd detected as wrapped
  • Replaces fragile startsWith("snip ") single-segment check

Fix #16 — Quiet mode

  • Adds SNIP_QUIET_NO_FILTER=1 to all wrapped commands
  • Eliminates "snip: no filter for X" token waste

Bonus: Shell keywords

  • for, while, if, case, done, etc. added to UNPROXYABLE_COMMANDS
  • Prevents snip from wrapping shell keyword constructs (matches snip's own unproxyableReason)

Test

30/30 pass | TypeScript typecheck ok | DeepSeek Pro reviewed

EnRaiha added 2 commits June 2, 2026 11:49
VincentHardouin#15, VincentHardouin#16)

- Replace startsWith("snip ") with segment-aware alreadyWrapped() that
  scans compound commands (&&, ||, ;, &) for existing snip wrapper
- Add SNIP_QUIET_NO_FILTER=1 env var prefix to eliminate "no filter" noise
- Add shell loop/conditional keywords to UNPROXYABLE_COMMANDS
Copilot AI review requested due to automatic review settings June 2, 2026 04:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates the Bash pre-execution hook to wrap eligible commands with snip more safely, while avoiding wrapping shell built-ins/control structures and running snip in a quiet/no-filter mode.

Changes:

  • Prefix wrapped commands with SNIP_QUIET_NO_FILTER=1 snip ... instead of snip ...
  • Expand the “do not wrap” command set to include shell keywords/control-flow tokens
  • Add alreadyWrapped() guard to skip rewriting commands that appear to already include snip

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/index.ts Alters bash command rewriting: new env prefix, broader unproxyable set, and new “already wrapped” detection
src/index.test.ts Updates expectations to match the new SNIP_QUIET_NO_FILTER=1 prefix behavior

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/index.ts
Comment on lines +62 to 71
function alreadyWrapped(command: string): boolean {
const segments = command.split(OPERATOR_RE);
return segments.some((s) => {
const trimmed = s.trimStart();
return (
trimmed.startsWith("snip ") ||
trimmed.replace(ENV_VAR_RE, "").startsWith("snip ")
);
});
}
Comment thread src/index.ts
Comment on lines +78 to +80
const command = output.args.command;
if (!command || typeof command !== "string") return;
if (alreadyWrapped(command)) return;
Comment thread src/index.ts
Comment on lines 82 to 88
if (findFirstPipe(command) !== -1) {
const pipeIdx = findFirstPipe(command)
const firstCmd = command.slice(0, pipeIdx).trimEnd()
const rest = command.slice(pipeIdx)
output.args.command = snipCommand(firstCmd) + ' ' + rest
return
const pipeIdx = findFirstPipe(command);
const firstCmd = command.slice(0, pipeIdx).trimEnd();
const rest = command.slice(pipeIdx);
output.args.command = snipCommand(firstCmd) + " " + rest;
return;
}
Comment thread src/index.ts
if (command.startsWith("snip ")) return
const command = output.args.command;
if (!command || typeof command !== "string") return;
if (alreadyWrapped(command)) return;
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR overhauls the snip-wrapping logic with three improvements: segment-aware already-wrapped detection (alreadyWrapped()), quiet-mode suppression via SNIP_QUIET_NO_FILTER=1 injected before snip, and protection of shell keyword constructs by extending UNPROXYABLE_COMMANDS.

  • Snowball fix (snips are getting more and more stacked #15): alreadyWrapped() splits on compound operators and inspects every segment (including those with env-var prefixes) for an existing snip prefix, replacing the fragile top-level startsWith check.
  • Quiet mode (Unconfigured snip creates more noise than what it suppresses #16): All calls to snipCommand now emit SNIP_QUIET_NO_FILTER=1 snip … instead of bare snip …, eliminating "no filter for X" noise; snip itself is also added to UNPROXYABLE_COMMANDS as a defense-in-depth layer.
  • Shell keywords: for, while, if, do, done, then, elif, else, fi, case, esac, until, select are added so that one-liner compound statements are left completely untouched.

Confidence Score: 5/5

Safe to merge — core wrapping logic is correct, all new paths are covered by tests, and previously flagged gaps have been addressed.

The three changes are self-contained and well-tested: 30 tests cover the new SNIP_QUIET_NO_FILTER=1 format, the alreadyWrapped segment scanning, and the full list of shell keyword guards including do. No regressions were introduced in the existing operator-splitting or pipe-detection paths.

No files require special attention.

Important Files Changed

Filename Overview
src/index.ts Introduces alreadyWrapped() for segment-aware snowball prevention, prepends SNIP_QUIET_NO_FILTER=1 to all wrapped commands, and extends UNPROXYABLE_COMMANDS with shell keywords and snip itself as defense-in-depth.
src/index.test.ts Updates all expected strings to the new SNIP_QUIET_NO_FILTER=1 snip format, adds a dedicated already-wrapped detection suite, and adds a one-liner loop test covering the new do keyword guard.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[toolExecuteBefore] --> B{tool === 'bash'?}
    B -- No --> Z[return, no change]
    B -- Yes --> C{alreadyWrapped?}
    C -- Yes --> Z
    C -- No --> D{findFirstPipe != -1?}
    D -- Yes --> E[snipCommand firstCmd + pipe rest unchanged]
    E --> Z
    D -- No --> F[split by OPERATOR_RE]
    F --> G{single segment?}
    G -- Yes --> H[snipCommand whole command]
    H --> Z
    G -- No --> I[map segments]
    I --> J{segment matches OPERATOR_RE?}
    J -- Yes --> K[keep operator as-is]
    J -- No --> L[snipCommand segment]
    K & L --> M[join all segments]
    M --> Z
Loading

Reviews (2): Last reviewed commit: "fix: add missing do keyword + tests for ..." | Re-trigger Greptile

Comment thread src/index.ts
Comment thread src/index.ts
Comment thread src/index.ts
Comment on lines +62 to 71
function alreadyWrapped(command: string): boolean {
const segments = command.split(OPERATOR_RE);
return segments.some((s) => {
const trimmed = s.trimStart();
return (
trimmed.startsWith("snip ") ||
trimmed.replace(ENV_VAR_RE, "").startsWith("snip ")
);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 alreadyWrapped short-circuits on any wrapped segment, silently skipping others

segments.some() returns true the moment a single segment is found to contain a snip prefix, which bails out for the whole command. If a command arrives with only its first segment wrapped — e.g., SNIP_QUIET_NO_FILTER=1 snip cmd1 && cmd2alreadyWrapped returns true and cmd2 is never monitored. This is an intentional snowball-prevention trade-off, but it means partially-wrapped commands silently pass through unmonitored.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread src/index.ts
Comment thread src/index.ts
const bareCmd = command.slice(envPrefix.length).trim();
if (!bareCmd) return command;
if (UNPROXYABLE_COMMANDS.has(bareCmd.split(/\s+/)[0])) return command;
return `${envPrefix}SNIP_QUIET_NO_FILTER=1 snip ${bareCmd}`;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the contribution, but I don't think it's a good idea to make that choice for everyone. I'd rather everyone be able to choose for themselves. Here, we're overriding the behavior for everyone.

@EnRaiha
Copy link
Copy Markdown
Author

EnRaiha commented Jun 2, 2026

Thanks for the feedback! You're right that forcing quiet mode on everyone isn't ideal.

Here's my revised thinking:

  1. Shell keywords fix (do etc.) — this is a straight bug fix. Without do in UNPROXYABLE_COMMANDS, one-liner loops like for i in 1 2 3; do echo $i; done produce a bash syntax error every time an AI generates them. It doesn't change anyone's UX — just prevents crashes. Happy to keep this one.

  2. Segment-aware alreadyWrapped() — you're better positioned to decide how to handle this. The Greptile P2 about mixed wrapped/unwrapped segments is valid, and I don't want to push something opinionated into your codebase. Happy to drop this from the PR.

  3. SNIP_QUIET_NO_FILTER=1 injection — agree, shouldn't be forced on everyone. I can revise it to be opt-in via an env var like SNIP_QUIET_MODE=true. When unset (default), behavior is identical to current — no quiet mode. Users who want silence opt in explicitly.

Want me to update the PR with just (1) shell keywords fix + (3) quiet mode as opt-in via env var? Or would you prefer them as separate PRs?

@VincentHardouin
Copy link
Copy Markdown
Owner

Thanks for the feedback! You're right that forcing quiet mode on everyone isn't ideal.

Here's my revised thinking:

  1. Shell keywords fix (do etc.) — this is a straight bug fix. Without do in UNPROXYABLE_COMMANDS, one-liner loops like for i in 1 2 3; do echo $i; done produce a bash syntax error every time an AI generates them. It doesn't change anyone's UX — just prevents crashes. Happy to keep this one.
  2. Segment-aware alreadyWrapped() — you're better positioned to decide how to handle this. The Greptile P2 about mixed wrapped/unwrapped segments is valid, and I don't want to push something opinionated into your codebase. Happy to drop this from the PR.
  3. SNIP_QUIET_NO_FILTER=1 injection — agree, shouldn't be forced on everyone. I can revise it to be opt-in via an env var like SNIP_QUIET_MODE=true. When unset (default), behavior is identical to current — no quiet mode. Users who want silence opt in explicitly.

Want me to update the PR with just (1) shell keywords fix + (3) quiet mode as opt-in via env var? Or would you prefer them as separate PRs?

I think the smaller the PRs are, the easier they are to review.

Your PR is trying to fix something that has already been addressed by snip via snip check and snip run (1) and PRs #13 and [#14] are trying to implement this in the code, but I think it's too complex to be merged as-is because it solves too many things.

In my opinion, this plugin should do as little as possible. That is to say:

  • check that snip is present
  • install the plugin
  • use snip check and snip run

@EnRaiha
Copy link
Copy Markdown
Author

EnRaiha commented Jun 2, 2026

Fair point. You're right that snip check and snip run handle this better, and the plugin should stay minimal. I'll close this PR.

Thanks for the detailed review — appreciate the explanation.

@EnRaiha EnRaiha closed this Jun 2, 2026
@EnRaiha
Copy link
Copy Markdown
Author

EnRaiha commented Jun 3, 2026

Closing this in favor of PR #14 (lenucksi) which follows your plugin philosophy — snip check/snip run instead of hardcoded lists. It addresses #6, #7, #15, #16 properly.

My shell keyword fix was a band-aid. PR #14 is the real solution. Thanks for the guidance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unconfigured snip creates more noise than what it suppresses snips are getting more and more stacked

3 participants