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:
- Use a TUI editor --
vi,nano,emacs -nwread the TTY directly and need no cross-process IPC:export VISUAL=nano - 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 *"] } } - Point
$EDITORat 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.
Metadata
Release files for scissors 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| scissors-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details |
| scissors-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| scissors-0.2.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| scissors-0.2.0-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 1.5 MB
Release files / scissors-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | scissors-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 388.0 kB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
ec0e9bd375c3ddbf7b89970a010271cf5a3b3f7898df51ce695b6a19092d6235
|
|
BLAKE2b-256 checksum How to use checksums |
d2c7bcfc77348012a600a0cc10ad56b3c40740192d0a264792f80047c0c6783e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency logRelease files / scissors-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | scissors-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 363.8 kB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
6a6868507490233220544c68eeb9ed90d519deec6f71524a7b414ba1622a9df3
|
|
BLAKE2b-256 checksum How to use checksums |
08cba0c6ac9612e452216ba8f85d53c7a92303656685096f437de3fb51ab93f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency logRelease files / scissors-0.2.0-py3-none-macosx_11_0_arm64.whl
| Download URL | scissors-0.2.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 347.2 kB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
74875bd2e990929c68478eb584ec4958277115a61a028dbeaa14ee1b776c49ab
|
|
BLAKE2b-256 checksum How to use checksums |
b6eae68e786a44e63c059b65de2e3b53ba90359fe97c732f5b3ecfec53af232b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency logRelease files / scissors-0.2.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | scissors-0.2.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 368.6 kB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
280b433894b75c92a477e96c319fe5e9b6519290cbfa645b9346bd59e9d43c77
|
|
BLAKE2b-256 checksum How to use checksums |
7483a161405e75dfac64e407885f1180f2e0372152ca110810576a0b4604c6c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency logSecurity for scissors 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 4 files in this release have been uploaded with provenance metadata.
Attestation for scissors-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.