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. 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, 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).

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)

use scissors::{approve_in_editor, Outcome};

match approve_in_editor("draft content", Some("context"))? {
    Outcome::Approved(text) => println!("approved: {text}"),
    Outcome::Aborted { draft_path } => eprintln!("aborted: {}", draft_path.display()),
}

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.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (387.9 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

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

Uploaded Python 3manylinux: glibc 2.17+ ARM64

scissors-0.1.0-py3-none-macosx_11_0_arm64.whl (347.1 kB view details)

Uploaded Python 3macOS 11.0+ ARM64

scissors-0.1.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.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for scissors-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 b414d030a6ede74fa60ea9776fdd0c5c4ec8aad87aa0ea9a937a3264a60864e3
MD5 e3ad4ea92dce43dc2f62ba2a6d2791d7
BLAKE2b-256 4e28abc4729fb892eaf69954873baa3a40c315c0ff9d4189602ab51590b8785d

See more details on using hashes here.

Provenance

The following attestation bundles were made for scissors-0.1.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.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for scissors-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 50ecc7269f1a785a8bb7cf7f4d849da9eb2e4ca31443c38adf2604696498798a
MD5 4a4a56071f85123ad4329c751390a5c6
BLAKE2b-256 1aec230596841efff7fc918eb6ea3cc955c8d84dd6db86f573b3aeef828327a5

See more details on using hashes here.

Provenance

The following attestation bundles were made for scissors-0.1.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.1.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for scissors-0.1.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 7f834987539a0cdae571bed9b34106f9354714062fc650f0f01b5fe2095d4836
MD5 12d203a8c13ecd8cd9a96272c21b62c9
BLAKE2b-256 3653a5f92101598bb322449e9801e53ed1042dad9f154d31c7f87fe318e5a357

See more details on using hashes here.

Provenance

The following attestation bundles were made for scissors-0.1.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.1.0-py3-none-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for scissors-0.1.0-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 1e93d7d0a8f149512dba9e97f27369980197467fc5aadf7a15a2ab6ea3b6d378
MD5 2dc618174fe6d70e377a16aa99c36259
BLAKE2b-256 968710ccc6c2e754c939e655f34f7252f296b8ac782f80044f75e5d0eb7a03b9

See more details on using hashes here.

Provenance

The following attestation bundles were made for scissors-0.1.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