Skip to main content

Local-only shell wrapper that documents an interactive remote-shell session into a per-host Markdown file.

Project description

scribe

A local-only shell wrapper that documents an interactive remote-shell session (ssh, proxychains+ssh, …) into a per-host Markdown file as you work, with rlwrap line editing (-a -c -A -r) on top. Nothing ever runs on the remote host — scribe only observes what you type and what the child prints. See CLAUDE.md for the full design, the local-only invariant, and the phase-by-phase build.

Requirements

  • rlwrap (sudo dnf install rlwrap / apt install rlwrap)
  • script (util-linux; used for the raw-log safety net — optional, auto-detected)
  • Python ≥ 3.11

Install

pipx install .                       # from a checkout (recommended)
pipx install git+<repo-url>          # or straight from git, no checkout needed

pipx puts scribe on your PATH in its own isolated venv — nothing leaks into your system Python. A plain pip install . (or pip install -e .) works too if you'd rather manage the environment yourself.

rlwrap execs the filter module by path, so it must stay executable. A wheel install (what pipx builds) can drop that +x bit, so scribe re-asserts it at startup — no editable install required. The filter runs under whatever python3 resolves to at runtime and imports the installed scribe package; that needs Python ≥ 3.11 on PATH, same as the launcher.

Use it — capture

Put scribe in front of however you connect. The rest of the line is passed through verbatim (your own pc/ssho/sshpass wrappers, whatever — scribe never interprets them):

scribe pc 10.10.5.4:3492 sshpass -p 'secretpassword' ssho root@192.168.178.2 -D 5001 /bin/sh

While you work, scribe writes 192.168.178.2.md in the current directory (IP picked from the user@host target; falls back to session-<timestamp> if none), rewritten atomically after every command — so a kill -9 never loses a committed command. It also keeps a flushed raw typescript 192.168.178.2.<timestamp>.raw alongside as ground-truth backup.

The .md has ## access (your exact invocation), ## enumeration (a placeholder, filled later by AI), and ## commands — each distinct command (arguments matter) with its latest output, ordered by a sensible heuristic (identity → users → network → processes → …), long output folded into <details>.

  • Ctrl-T pauses/resumes recording mid-session (shows [scribe: recording OFF/ON]); the .raw log keeps going regardless. Set SCRIBE_NO_HOTKEY=1 to keep your own Ctrl-T binding.
  • Reconnecting to the same host merges into the same .md (last output wins); a different host that collides on a filename gets -2, never an overwrite.

Use it — enumerate (offline AI)

scribe fills every mechanical section; the ## enumeration prose is written later, offline, by opencode + a small model (e.g. qwen). It reads only the file scribe produced — still local.

# in opencode, with 192.168.178.2.md in the working dir:
/enumerate 192.168.178.2.md

…or paste prompts/enumerate.md into any model. It fills only the region between the <!-- SCRIBE:ENUM:START/END --> markers with a terse, evidence-cited summary (OS, privilege, users, network/pivot, egress, security stack, monitoring, verdict), writing not observed rather than guessing. Edit enum_signatures.txt to add the AV/EDR/monitoring process names specific to your estates — scribe pre-greps recorded output for them and drops a scribe-signals hint into the file for the model to lean on.

Single-file deploy (no pip install on the target)

For a target host you don't want to (or can't) install anything Python-packaging-shaped on, build one self-contained executable file and scp it over instead:

python scripts/build_release.py                       # writes dist/scribe
scp dist/scribe user@target:~/scribe
ssh user@target 'chmod +x ~/scribe && ~/scribe pc ... ssho ...'

dist/scribe is a plain .py file (with a #!/usr/bin/env python3 shebang) that base64-embeds the entire scribe package — the target only needs python3 and rlwrap (and, optionally, script), nothing else from this repo. It works by pointing rlwrap's -z filter back at itself: run normally, it execs rlwrap ... -z <its own name>; rlwrap then re-execs that same file as the filter subprocess, which dispatches into scribe.filter.main() instead of scribe.cli.main() the second time round (see CLAUDE.md, "Phase 6" for the full mechanism). Nothing about what scribe does or sends changes — this is purely a packaging trick, proven end to end by tests/test_build_release.py's bundle integration test.

  • ./scribe --dump-enum writes the embedded prompts/enumerate.md and enum_signatures.txt into the current directory — handy on a host with nothing else from the scribe repo on it.
  • python scripts/build_release.py --check dist/scribe regenerates the bundle in memory from the current source tree and diffs it against an existing release file (non-zero exit on a mismatch) — confirms a previously-built dist/scribe still matches main before you ship it.

Environment knobs

Var Effect
SCRIBE_RLWRAP path to the rlwrap binary (default rlwrap)
SCRIBE_SCRIPT path to script (default script)
SCRIBE_NO_RAW disable the .raw typescript safety net
SCRIBE_NO_HOTKEY don't bind Ctrl-T (keep your own inputrc binding)
SCRIBE_FILTER_DIR override where rlwrap looks for the filter (default: the package's own directory) — set automatically by the single-file bundle; not normally needed by hand
SCRIBE_FILTER_NAME override the bare filename passed to rlwrap's -z (default filter.py) — ditto
SCRIBE_RUN_AS_FILTER internal: set automatically so a re-exec'd single-file bundle knows to run as the filter, not the launcher

Licensing

scribe is MIT-licensed (see LICENSE) — Copyright (c) 2026 Philipp Herkert.

Every file in this repository is scribe's own code. scribe/_filterproto.py is an independent implementation of rlwrap's filter wire protocol (the length-prefixed tagged-message exchange over rlwrap's -z pipes) — written from the protocol itself, not derived from rlwrap's own GPL filter library — so scribe carries no third-party copyleft. scribe still runs under rlwrap (a separate program you install), but ships none of its code.

Project details


Download files

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

Source Distribution

scribesh-1.0.0.tar.gz (50.7 kB view details)

Uploaded Source

Built Distribution

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

scribesh-1.0.0-py3-none-any.whl (28.8 kB view details)

Uploaded Python 3

File details

Details for the file scribesh-1.0.0.tar.gz.

File metadata

  • Download URL: scribesh-1.0.0.tar.gz
  • Upload date:
  • Size: 50.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for scribesh-1.0.0.tar.gz
Algorithm Hash digest
SHA256 c3d2f16e68faf9ea3d26aaf56762f3c214c10d75dfadc795a8d544b42448c5eb
MD5 7878c630938e8bf095e3840195eff1e8
BLAKE2b-256 7bfa76e65cc89fb980c4bf29df2dc81901f3ce3d2cae0d2fc969f4dfea54a948

See more details on using hashes here.

Provenance

The following attestation bundles were made for scribesh-1.0.0.tar.gz:

Publisher: release.yml on BearlyStable/scribe

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

File details

Details for the file scribesh-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: scribesh-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 28.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for scribesh-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d4602b16240f5bb7d2dcc502612f102d29e0170ca1552dc22b0e01fe47bdaf0a
MD5 774f1a0efb6aad7d4e813c6f6d5f87b9
BLAKE2b-256 0b3fcec04d75c27c2994b3957f303ee8ab098f7a7ce6c0894222a6b595d33957

See more details on using hashes here.

Provenance

The following attestation bundles were made for scribesh-1.0.0-py3-none-any.whl:

Publisher: release.yml on BearlyStable/scribe

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