Skip to main content

Editor-based content approval, git-commit style

Project description

scissors

crates.io PyPI CI License: MIT OR Apache-2.0

scissors is a Unix CLI primitive for editor-based content approval, modelled on git's commit.cleanup=scissors convention. It opens your content in your editor and keeps the approved bytes (everything above the scissors line). Use it as a stdin->stdout filter, or point it at a file to edit in place.


Install

pip install scissors        # PyPI (standalone binary, no Python needed at runtime)
uv tool install scissors    # uv
cargo install scissors      # crates.io

Usage

# Approve a PR body before submission
gh pr view 42 --json body --jq .body | scissors > approved.md && \
  gh pr edit 42 --body-file approved.md

# Edit and confirm a draft, with context for multi-draft sessions
echo "$DRAFT" | scissors --context "Issue #26 reply" > /tmp/out

# Non-interactive: approve as-is, no editor (CI, scripts, agents)
echo "$DRAFT" | scissors --yes > /tmp/out

The editor opens with your content followed by a scissors line:

your content here

# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Everything below is context and will be stripped from the final content.
# ...

Edit the content above the line, save, and close. Everything below the scissors line is discarded.

The editor buffer

scissors opens the draft as a file named COMMIT_EDITMSG, the same name git commit uses. Editors that dim git commit messages (VS Code, Vim, Emacs with Magit, ...) dim this buffer the same way, so the # footer reads as greyed-out comments while you edit and the body stays plain. The cut is located by the >8 marker, so the footer is detected and stripped even if its # prefix gets altered.

File mode

Pass a file to edit it in place, like $EDITOR <file>:

scissors notes.md          # opens notes.md, edits it in place

On approve, the approved content atomically replaces the file. On abort (you emptied the buffer above the scissors line) or on any error, the file is left exactly as it was: editing happens in a COMMIT_EDITMSG sidecar in a temp directory beside the target, and the file is never touched until the final atomic rename, so it is never left half-written even if scissors is interrupted mid-edit. The atomic replace gives the file a new inode, so hard links break and a symlink is written through to its target. The mode bits are copied onto the new inode, but owner, group, ACLs and extended attributes reset to the editing user's defaults, so avoid sudo scissors <file> on a file whose ownership or security context must be preserved. scissors prints the sidecar path to stderr at launch, so you can reopen it if you lose the editor window. The outcome is signalled by the exit code (0/1/2); nothing is written to stdout in this mode.

To review a file's content without editing it in place, pipe it through the stdin filter instead: scissors < notes.md > approved.md.

In-place mode suits agents and scripts: write the draft to a path, run a bare scissors <path> (easy to put on an allowlist), then read the file back. No pipe and no command substitution are required.

Pass - as the file (scissors -) to force the stdin/stdout path explicitly; omitting the argument does the same.

Exit codes

Code Meaning
0 approved -- file written in place (file mode), or content on stdout (stdin)
1 aborted -- you emptied the content above the scissors line
2 error -- no editor available, editor failed, non-UTF-8 input, or I/O error

In stdin mode, on abort or error the draft tempfile is preserved and its path is printed to stderr. In file mode, on abort or error the file is left unchanged (the edit happens in a sidecar that is discarded).

Editor resolution

scissors uses $VISUAL, then $EDITOR, then falls back to vi. For GUI editors, set a blocking flag so the process waits for you to close the file:

export VISUAL="code --wait"
export VISUAL="subl -w"

Non-interactive / headless use

scissors is fail-closed: without an editor it errors (exit 2) rather than silently approving. For environments with no human at a terminal (CI, scripts, AI agents), pass --yes to approve the input as-is without opening an editor:

echo "$DRAFT" | scissors --yes   # approve verbatim, exit 0

This mirrors git commit (interactive editor vs -m/--no-edit): the same pipeline works interactively (edit) and unattended (--yes).

One asymmetry to note for scripts: with --yes, empty stdin is approved as-is (exit 0), but an empty or whitespace-only file is rejected (exit 1), since there is nothing to approve in place.

Running under a sandbox

Some environments (AI agents, restricted CI, Claude Code) run commands in a sandbox that blocks the IPC between a GUI editor's CLI (code --wait, cursor --wait, subl -w) and the running editor process. The CLI then returns immediately without opening anything, and scissors reports a silent-failure error (exit 2). Three ways to make it work inside a sandbox:

  1. Use a TUI editor -- vi, nano, emacs -nw read the TTY directly and need no cross-process IPC: export VISUAL=nano
  2. Exclude the GUI editor from the sandbox -- if your sandbox has an allowlist (e.g. Claude Code's sandbox.excludedCommands), add the binary: { "sandbox": { "excludedCommands": ["code *"] } }
  3. Point $EDITOR at a helper the host can route to an editor running outside the sandbox.

scissors stays sandbox-agnostic: it honours $EDITOR like any Unix tool and surfaces a clear error when the editor never opens.

Library usage (Rust)

Both entry points take an Options builder so new knobs stay additive. Input is UTF-8 text only; non-UTF-8 bytes surface as an error before anything is written.

use scissors::{approve_in_editor, Options, Outcome};

// stdin-style: returns the approved text
match approve_in_editor("draft content", &Options::new().context("Issue #26 reply"))? {
    Outcome::Approved(text) => println!("approved: {text}"),
    Outcome::Aborted { draft_path } => eprintln!("aborted: {}", draft_path.display()),
}
use std::path::Path;
use scissors::{approve_file_in_place, FileOutcome, Options};

// in-place: the approved content is written back to the file
match approve_file_in_place(Path::new("notes.md"), &Options::new())? {
    FileOutcome::Approved => println!("notes.md updated"),
    FileOutcome::Aborted => eprintln!("aborted; notes.md unchanged"),
}

License

Dual-licensed under MIT or Apache-2.0 at your option.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

scissors-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (388.0 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

scissors-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (363.8 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

scissors-0.2.0-py3-none-macosx_11_0_arm64.whl (347.2 kB view details)

Uploaded Python 3macOS 11.0+ ARM64

scissors-0.2.0-py3-none-macosx_10_12_x86_64.whl (368.6 kB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file scissors-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for scissors-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 ec0e9bd375c3ddbf7b89970a010271cf5a3b3f7898df51ce695b6a19092d6235
MD5 74527e81e7261d11c736c1fe8f788c3d
BLAKE2b-256 d2c7bcfc77348012a600a0cc10ad56b3c40740192d0a264792f80047c0c6783e

See more details on using hashes here.

Provenance

The following attestation bundles were made for scissors-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release.yml on ggueret/scissors

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file scissors-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for scissors-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 6a6868507490233220544c68eeb9ed90d519deec6f71524a7b414ba1622a9df3
MD5 8abdc8b3ff92da4c89ea0f9910113009
BLAKE2b-256 08cba0c6ac9612e452216ba8f85d53c7a92303656685096f437de3fb51ab93f6

See more details on using hashes here.

Provenance

The following attestation bundles were made for scissors-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release.yml on ggueret/scissors

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file scissors-0.2.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for scissors-0.2.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 74875bd2e990929c68478eb584ec4958277115a61a028dbeaa14ee1b776c49ab
MD5 b34a41ca130e382c1a3e1d6fa636b294
BLAKE2b-256 b6eae68e786a44e63c059b65de2e3b53ba90359fe97c732f5b3ecfec53af232b

See more details on using hashes here.

Provenance

The following attestation bundles were made for scissors-0.2.0-py3-none-macosx_11_0_arm64.whl:

Publisher: release.yml on ggueret/scissors

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file scissors-0.2.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for scissors-0.2.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 280b433894b75c92a477e96c319fe5e9b6519290cbfa645b9346bd59e9d43c77
MD5 ae7a65ba32d0f83d78fa081e27e305db
BLAKE2b-256 7483a161405e75dfac64e407885f1180f2e0372152ca110810576a0b4604c6c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for scissors-0.2.0-py3-none-macosx_10_12_x86_64.whl:

Publisher: release.yml on ggueret/scissors

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page