# Stdin/pipe input support

- **Pattern:** `ab-000647` (`cli-quality.io-behavior.stdin-pipe`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000647
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000647)

## Why it matters

Unix philosophy is composition, and composition requires stdin. When `mytool validate` only accepts `--file path.json` and refuses piped input, users cannot run `curl api.example.com/data | mytool validate` or chain your tool into generators. Tools that cannot participate in pipelines get replaced by tools that can. Worse, stdin reading without TTY detection causes interactive invocations to hang forever — the user sees a frozen terminal and assumes the tool crashed.

## Severity rationale

Medium because missing stdin support blocks pipeline composition without causing data loss or crashes.

## Remediation

Accept `-` as a filename meaning stdin, or auto-detect `!process.stdin.isTTY` and read from the pipe. Implement once in `src/cli/lib/input.ts`:

```ts
async function getInput(filePath?: string) {
  if (filePath && filePath !== '-') return readFileSync(filePath, 'utf8')
  const chunks: Buffer[] = []
  for await (const chunk of process.stdin) chunks.push(chunk)
  return Buffer.concat(chunks).toString('utf8')
}
```

## Detection

- **ID:** `stdin-pipe`
- **Severity:** `medium`
- **What to look for:** Count all input-accepting commands. For each, check if commands that accept file input can also read from stdin (pipe). Look for `-` as a filename convention (e.g., `mytool process -` reads from stdin). Check for `process.stdin`, `sys.stdin`, `os.Stdin`, or `std::io::stdin()` usage. Verify the tool can participate in Unix pipelines (e.g., `cat data.json | mytool validate`).
- **Pass criteria:** At least one command that accepts input can read from stdin/pipe, either via `-` filename convention or automatic stdin detection when no file argument is given. The CLI correctly handles both TTY (interactive) and non-TTY (piped) stdin — at least 50% of input commands should accept stdin for pipeline composition. Report: "X input commands found, Y accept stdin/pipe input."
- **Fail criteria:** Commands that accept file input only accept file paths — no way to pipe data in. Or stdin reading is attempted but blocks indefinitely when no pipe is connected.
- **Skip (N/A) when:** The CLI does not accept data input — it only takes configuration options and performs actions. All checks skip when no CLI entry point is detected.
- **Detail on fail:** `"'validate' command requires a --file argument — cannot read from stdin/pipe"` or `"stdin reading implemented but hangs when run interactively without pipe input (no TTY detection)"`
- **Remediation:** Pipe support makes your CLI composable with other tools:

  ```ts
  // Node.js — read from file or stdin
  async function getInput(filePath?: string): Promise<string> {
    if (filePath && filePath !== '-') {
      return readFileSync(filePath, 'utf8')
    }
    // Read from stdin
    const chunks: Buffer[] = []
    for await (const chunk of process.stdin) {
      chunks.push(chunk)
    }
    return Buffer.concat(chunks).toString('utf8')
  }
  ```

  ```python
  # click — use click.File to handle both
  @cli.command()
  @click.argument('input', type=click.File('r'), default='-')
  def validate(input):
      """Validate JSON input. Reads from stdin if no file given."""
      data = input.read()
  ```

Taxons: user-experience

HTML version: https://auditbuffet.com/patterns/ab-000647
