Reading from stdin

gnaw can take its input from standard input instead of walking a directory. Piped stdin is read once and classified into one of two modes:

  • Path list โ€” one repo-relative path per line; gnaw sources exactly those files. This is the git diff --name-only | gnaw workflow.
  • Content โ€” the piped bytes are treated as a single synthetic file named stdin, scanned and rendered like any other file. This is the openssl genpkey | gnaw --secret-scan warn workflow.

This page is the precise contract; the how-to guides cover the day-to-day workflows (piping a file list, piping content).

git diff --name-only | gnaw          # path list
cat config.env | gnaw --secret-scan warn   # content

When stdin mode activates

gnaw reads stdin at all only when all of these hold:

ConditionWhy
stdin is not a terminal (it's piped or redirected)A human at a prompt isn't sending input
no path argument was givenAn explicit path means "walk this"; it always wins
not in TUI mode (--tui)The TUI drives its own selection
not the internal clipboard daemonThe daemon uses stdin for its own payload

If any condition fails, gnaw behaves as before. In particular, a bare gnaw at an interactive terminal prints help, and gnaw . (or any explicit path) walks the tree even inside a pipeline.

How gnaw chooses path list vs content

Once gnaw has decided to read stdin, it classifies the input:

If any non-blank line resolves to an existing file under the root, the input is a path list. Otherwise it's content.

The check is deliberately asymmetric. A real path list has lines that name real files, so it takes only one resolving line to choose path mode โ€” which means every path-list workflow that worked before still works, including a git diff --name-only where most listed files were deleted (those lines fail to resolve, but any surviving file still tips the decision to path mode). Genuine content โ€” a PEM key, a log, a diff โ€” resolves zero lines and falls through to content mode.

gnaw prints the decision so a surprise is a one-glance diagnosis:

[i] stdin: treated as path list
[i] stdin: treated as content

The residual ambiguity is content whose lines happen to name real files (a build log that mentions src/main.rs, say) โ€” it classifies as a path list, most lines drop, and you get a mostly-empty run. The info line above is how you catch it; the force-modes below are how you fix it.

Forcing a mode

Two escape hatches override the sniff when you know better:

FormForces
gnaw - (or gnaw /dev/stdin)content โ€” read stdin as one file, no classification
gnaw --stdin-pathspath list โ€” treat every line as a path, even if none resolve

- follows the Unix convention (grep, cat, jq all read stdin on -). A bare gnaw - at an interactive terminal is an error, not a hang โ€” pipe something or pass a path.

How paths are resolved (path-list mode)

Each non-blank line is trimmed, joined onto the root, and canonicalized:

  • Relative paths resolve against the root (the path argument, default .).
  • Paths that resolve outside the root are dropped. After canonicalization, a path that doesn't sit under the root is discarded โ€” a piped ../../etc/passwd can't escape the allowed root.
  • Paths that don't exist are dropped. Deleted files (which still appear in git diff --name-only) fail to canonicalize and are skipped silently.
  • Binary and empty files are dropped during extraction, identical to a normal walk.

Blank lines are ignored, so trailing newlines and empty input are harmless; empty input yields an empty selection rather than an error.

Content mode

When stdin is classified as content, gnaw builds a prompt from a single synthetic file:

  • The file is named stdin and carries no extension, so language-aware stages (syntax-aware compression, chunking) fall back to plain text โ€” the only honest choice for bytes of unknown origin.
  • Secret scanning still runs. --secret-scan warn|redact|block applies to the piped content exactly as it would to a file, which is what makes openssl genpkey | gnaw --secret-scan warn a useful check.
  • Whitespace-only input yields an empty selection, not a one-line empty file โ€” the same policy as an empty file in a walk.
  • Input must be valid UTF-8. Piping binary (for example DER-encoded keys) fails while reading stdin rather than producing garbage.

Ordering (path-list mode)

The surviving files are sorted by path, not kept in the order they arrived on stdin. This keeps output byte-stable for snapshot tests and matches the other sources. If you need a specific order, it has to come from the rendered content, not the pipe order.

Interaction with other features

Filtering is bypassed. A piped path list is treated as authoritative โ€” you named exactly these files โ€” so --include / --exclude and .gitignore rules do not apply to it. Content mode has a single synthetic file, so filtering is moot there too. Binary/empty dropping and secret scanning still run in both.

Secret scanning still applies. The scrubber stage runs as normal, so --secret-scan=block will still halt a stdin run that hits a finding, and redact still masks โ€” in both path-list and content mode.

Stdin wins over the git source axes. If stdin supplied input, it takes precedence over --git-diff-shas and the git-narrative source selection. A run is either "this stdin input" or "this git range," not both.

--full-directory-tree is ignored. The source tree is always derived from the stdin input, so it lists exactly the piped files (or the single stdin file) and never expands to the whole repository.

Templates resolve normally. Because a stdin run has no --git-diff-shas, the default template (or your --template) is used. The git-narrative templates' {{git_diff}} section stays empty unless you also pass --diff, which loads the working-tree diff as chrome alongside the piped input.

See also