Skip to content

Add --porcelain flag for clean, pipeable commit-message output #6

Open

Description

@yegor256

Problem or motivation

commitbee is designed for interactive terminal use. While the LLM generates a response, its raw JSON is streamed to the terminal token-by-token, spinners and progress lines flow alongside it, and COMMITBEE_LOG=debug adds tracing on top. The final sanitized commit message only appears at the end, on top of all that noise. There is no clean way today to get just the commit message out of commitbee — every piping attempt either leaks noise through or has to silence legitimate error messages with 2>/dev/null.

Concrete use cases that are blocked today:

  • Piping the generated message into another tool — a validator like commitlint, a preview viewer, a formatter, a custom shell script.
  • Editor / IDE integrations (VS Code, JetBrains, nvim, etc.) that capture stdout and insert into their commit-message buffer.
  • Populating the commit-message file from a prepare-commit-msg hook without the current 2>/dev/null workaround that also hides real errors.
  • Higher-level automation (release-notes pipelines, AI-review chains, etc.) that expects exactly one clean string on stdout.

Example usage once shipped:

# Pipe the generated message through any tool — validator, formatter, preview
commitbee --porcelain | commitlint
commitbee --porcelain | bat -l gitcommit

# Use from any shell script
commitbee --porcelain | your-script.sh

# Populate git's commit-msg file from a prepare-commit-msg hook
# ($1 is the path git passes to the hook)
commitbee --porcelain > "$1"

The existing workaround — commitbee --dry-run --yes 2>/dev/null — is undiscoverable, isn't a documented contract, and silences legitimate errors the user would otherwise want to see.

Note on the original phrasing: the live token stream users see today is JSON (the LLM returns a JSON object and commitbee streams it token-by-token before the sanitizer turns it into a Conventional Commit message). The final stdout contract under --porcelain is the sanitized plain-text commit message — the user-facing artifact — not the raw LLM JSON.

Proposed solution

Add a new top-level flag --porcelain that puts commitbee into a machine-readable output mode.

stdout contract:

  • stdout contains exactly the final sanitized commit message followed by a single \n.
  • UTF-8, no BOM, no ANSI escape sequences, no terminal hyperlinks.
  • On any error, stdout is empty; process exits non-zero.

What --porcelain silences:

  • The live-streamed LLM JSON response (the main source of noise today).
  • All progress indicators and spinners.
  • All info / warning / status lines.
  • All tracing output, regardless of RUST_LOG / COMMITBEE_LOG.
  • ANSI color in all styled output, including error reports.
  • Terminal hyperlinks in error reports.

What --porcelain forces non-interactive:

  • Implies --dry-run — no commit is created; the message is printed and the process exits.
  • Implies --no-split — split-commit UI is interactive-only.
  • The Edit / Refine / Cancel review menu is unreachable under --dry-run.
  • Disables the interactive --allow-secrets confirmation — with --allow-secrets passed, porcelain falls through to the non-interactive "fail closed" branch if secrets are detected.

Incompatibilities (rejected at argument-parse time with exit code 2):

Combination Reason
--porcelain --yes --yes commits for real; --porcelain only generates and prints. Mutually exclusive output destinations.
--porcelain --clipboard stdout contract vs. clipboard destination
--porcelain --show-prompt swallowing a debug flag silently would be deceptive
--porcelain --verbose same reasoning
--porcelain -n <N> multi-candidate UI is pointless without a picker
--porcelain <subcommand> --porcelain is only meaningful for the default generate flow; it does not apply to config, doctor, completions, hook, init, set-key, get-key, or eval

Errors still flow to stderr (color and hyperlinks disabled under porcelain). The process exits non-zero, so downstream tools detect failure without parsing stdout.

Stability boundary:

  • The structural contract (one sanitized commit message on stdout, trailing \n, UTF-8, no decoration) is stable from v0.7.0 onward.
  • The content of the message (wording, capitalization, type/scope choices) depends on the configured LLM and is not stable across prompts, models, or commitbee versions.

Acceptance criteria

  • commitbee --porcelain writes exactly <message>\n to stdout on a successful generation.
  • commitbee --porcelain writes zero bytes to stderr on success, regardless of RUST_LOG, COMMITBEE_LOG, or TTY state.
  • commitbee --porcelain exits 0 on success and non-zero on any failure (no staged changes, LLM unreachable, secrets detected, cancelled, etc.).
  • commitbee --porcelain combined with any of --yes, --clipboard, --show-prompt, --verbose, -n <N>, or a subcommand exits with an argument-parse error (exit code 2) and empty stdout.
  • commitbee --porcelain --allow-secrets with staged secrets does not hang on interactive stdin; it fails closed with a non-zero exit and empty stdout.
  • commitbee --porcelain piped to a non-TTY consumer produces identical output to TTY (no spinner bleed-through, no ANSI).
  • Integration tests in tests/porcelain.rs cover each of the above using assert_cmd + wiremock + tempfile with env_clear().
  • A structural lint test walks src/ and fails if println! / print! appears outside a pinned allowlist of known emission sites (prevents future stdout leaks).

Alternatives considered

  • --dry-run alone: prints the final message to stdout but still streams the live LLM response and progress lines, and can drop into the interactive review menu in a TTY. Not a stable machine-readable contract.
  • Shell redirection (commitbee --dry-run --yes 2>/dev/null): the current workaround, but silences legitimate errors, is undiscoverable, and isn't a documented contract.
  • Parsing or grepping commitbee's current stdout: fragile; breaks whenever output formatting changes.
  • --just-body (original suggestion): ambiguous — a Conventional Commit message has both a subject and a body, so "just body" reads as "only the body paragraph, omitting the subject." Rejected.
  • --format=raw|json as a single knob: semantically awkward because --format=raw implies a format is being chosen when the intent is the absence of formatting. Structured JSON output is a separate future concern — see Related.

Area

CLI / UX

Related

  • Future structured output: a separate --format=<json|…> modifier is expected to land when structured JSON output becomes a feature. It will be orthogonal to --porcelain — one controls stdout cleanliness, the other controls output shape. --porcelain alone will always mean "plain-text commit message."
  • Milestone: v0.7.0

Issue originally reported by @yegor256; description refined by the maintainer.

Activity

  1. self-assigned this
    on Apr 19, 2026
  2. added
    enhancementNew feature or request
    and removed
    bugSomething isn't working
    on Apr 19, 2026
  3. changed the title [-]`--just-body` would be nice to have[/-] [+]Add --porcelain flag for clean, pipeable commit-message output[/+] on Apr 19, 2026
  4. Sephyi commented on Apr 19, 2026

    @Sephyi
    Owner

    Thanks for raising this, @yegor256 — this genuinely wasn't on my radar, but it's a real gap.

    I've edited your original issue description and title in place to reflect the implementation I've been planning out (flag name, stdout contract, edge cases around subcommands, secret handling, etc.). If any of the refined version doesn't match what you originally had in mind, please flag it and I'll adjust — don't want to accidentally drift from your actual intent.

    I’ll likely implement this feature within the next 24 hours on the development branch.

  5. yegor256 commented on Apr 19, 2026

    @yegor256
    Author

    @Sephyi looks perfect, waiting for the implementation

  6. added a commit that references this issue on Apr 19, 2026
    d5a9d38
  7. Sephyi commented on Apr 19, 2026

    @Sephyi
    Owner

    @yegor256 pushed to the development branch. If you don't already have CommitBee installed from there:

    cargo install --git https://github.com/Sephyi/commitbee --branch development commitbee
    

    Also covered in Docs Getting Started under Install → Development Version.

    Just let me know if you encounter any issues.


    Known follow-ups already on the radar (tracked here so I don’t forget):

    • Distinct exit codes per failure mode (no staged changes / secrets detected / provider unreachable) so scripts can dispatch without parsing stderr
    • Optional stderr debug escape hatch (COMMITBEE_PORCELAIN_DEBUG=1) for diagnosing CI failures without losing the clean-stdout contract
    • Sanitizer step to strip ANSI CSI escapes from LLM output, defending against models that emit colour codes inside the commit text
    • Full end-to-end integration test for the stdout-byte-equality contract (wiremock + git fixture)
    • --format=json structured-output modifier — orthogonal to --porcelain, landing when structured JSON output becomes its own feature
    • Refactor Progress to enum { Silent, Plain, Spinner } — replaces the current silent: bool + bar: Option<_> null-object pattern so future methods can't forget the silent-guard
    • Keep Cli immutable in App::new; introduce effective_* accessors instead of mutating parsed flags, for testability and clarity
    • Extract the porcelain-override logic as a pure function so it's unit-testable without building a full App + Config::load
  8. yegor256 commented on Apr 19, 2026

    @yegor256
    Author

    @Sephyi I'll wait until it's released

  9. Sephyi commented on May 3, 2026

    @Sephyi
    Owner

    @yegor256, I apologize for the delay. I genuinely want to ensure that all the issues raised in #3 are fully addressed in the next release. I’m not sure when it will be ready, but I thought of it and wanted to post a quick comment.

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions