From c59bb7f3228886a4f6c2bc7d24596f8a9d53b511 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 5 Oct 2026 15:27:11 +0200 Subject: [PATCH] fix: keep machine-readable output raw --- README.md | 5 +++-- llms.txt | 3 ++- package.json | 2 +- src/router.ts | 13 +++++++++++++ test/router.test.ts | 11 +++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fcdecba..95c439a 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,9 @@ The allowlist is generated from snip's own filters, pinned to the v0.25.2 releas files, 173 command/subcommand rules), plus anything you drop in `~/.config/snip/filters/`. `exclude_flags` and `require_flags` are matched the way snip matches them — against every argument, flag or not — so `git log --format=...`, `git diff -p` and `gh pr diff` all stay -raw, and `npm install` wraps while `npm view` does not. `#nosnip` anywhere in a command -skips the whole thing. +raw, and `npm install` wraps while `npm view` does not. Output another program parses also +stays raw: `--json`, `--jq`, `--template`, `--format json`, `-o json`, and `git show REV:path` +file contents. `#nosnip` anywhere in a command skips the whole thing. Filters are read once at startup. A new file in `~/.config/snip/filters/` needs an opencode restart before smartsnip routes to it. `smartsnip doctor` reports your installed diff --git a/llms.txt b/llms.txt index e5a534b..c3abd30 100644 --- a/llms.txt +++ b/llms.txt @@ -23,7 +23,8 @@ - Append `#nosnip` to any bash command when you need its full raw output. That is the only way to be certain nothing was dropped. - `git log --format=...`, `git status --porcelain` etc. are never filtered — explicit - format flags are respected. + format flags are respected. So are `--json`, `--jq`, `--template`, `--format json`, + `-o json` and `git show REV:path`. ## Files diff --git a/package.json b/package.json index cf98667..76ee80c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-smartsnip", - "version": "0.2.0", + "version": "0.2.1", "description": "Cuts shell-output tokens in opencode. Wraps only what snip can filter — everything else runs byte-identical.", "type": "module", "main": "dist/index.js", diff --git a/src/router.ts b/src/router.ts index a805675..b301264 100644 --- a/src/router.ts +++ b/src/router.ts @@ -63,6 +63,18 @@ function isDenied(info: SegmentInfo, config: SmartSnipConfig): boolean { ) } +const DATA_FLAGS = ["--json", "--jq", "--template"] +const FORMAT_FLAGS = ["--format", "--output", "-o"] + +/** Output another program will parse, or file contents (`git show REV:path`), must stay verbatim. */ +function isDataOutput(info: SegmentInfo): boolean { + const args = info.tokens.slice(1).map((a) => a.replace(/^(['"])(.*)\1$/, "$2")) + if (args.some((a) => DATA_FLAGS.some((flag) => a === flag || a.startsWith(`${flag}=`)))) return true + if (args.some((a, i) => FORMAT_FLAGS.some((flag) => a === `${flag}=json` || (a === flag && args[i + 1] === "json")))) + return true + return info.head === "git" && args[0] === "show" && args.some((a) => !a.startsWith("-") && a.includes(":")) +} + /** Decide whether a single analyzed segment should be wrapped with snip. */ export function shouldWrap( segment: string, @@ -76,6 +88,7 @@ export function shouldWrap( if (BUILTINS.has(info.head)) return null if (info.body.startsWith("(") || info.body.startsWith("{")) return null // subshell/group + if (isDataOutput(info)) return null const entry = table.get(info.head) if (!entry) return null if (!matchesEntry(entry, info)) return null diff --git a/test/router.test.ts b/test/router.test.ts index 3898f70..1c731a8 100644 --- a/test/router.test.ts +++ b/test/router.test.ts @@ -38,6 +38,17 @@ describe("basic wrapping", () => { expect(rw("bun run format")).toBe("bun run format") }) + test("machine-readable output passes through verbatim", () => { + expect(rw("gh pr list --json number,title")).toBe("gh pr list --json number,title") + expect(rw("gh pr view 12 --jq .title")).toBe("gh pr view 12 --jq .title") + expect(rw("gh run list --template '{{.name}}'")).toBe("gh run list --template '{{.name}}'") + expect(rw(`gh pr list "--json" number,title`)).toBe(`gh pr list "--json" number,title`) + expect(rw(`gh pr list '--json=number,title'`)).toBe(`gh pr list '--json=number,title'`) + expect(rw("docker ps --format json")).toBe("docker ps --format json") + expect(rw("git show origin/main:src/app.ts")).toBe("git show origin/main:src/app.ts") + expect(rw("git show HEAD")).toBe("snip git show HEAD") + }) + test("builtins never wrapped (issue #6)", () => { expect(rw("cd /tmp")).toBe("cd /tmp") expect(rw("source .venv/bin/activate")).toBe("source .venv/bin/activate")