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.rawlog keeps going regardless. SetSCRIBE_NO_HOTKEY=1to 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-enumwrites the embeddedprompts/enumerate.mdandenum_signatures.txtinto the current directory — handy on a host with nothing else from the scribe repo on it.python scripts/build_release.py --check dist/scriberegenerates 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-builtdist/scribestill matchesmainbefore 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c3d2f16e68faf9ea3d26aaf56762f3c214c10d75dfadc795a8d544b42448c5eb
|
|
| MD5 |
7878c630938e8bf095e3840195eff1e8
|
|
| BLAKE2b-256 |
7bfa76e65cc89fb980c4bf29df2dc81901f3ce3d2cae0d2fc969f4dfea54a948
|
Provenance
The following attestation bundles were made for scribesh-1.0.0.tar.gz:
Publisher:
release.yml on BearlyStable/scribe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
scribesh-1.0.0.tar.gz -
Subject digest:
c3d2f16e68faf9ea3d26aaf56762f3c214c10d75dfadc795a8d544b42448c5eb - Sigstore transparency entry: 2269132526
- Sigstore integration time:
-
Permalink:
BearlyStable/scribe@0072be7823f6139debefdf105c032ae98ba31b33 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/BearlyStable
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0072be7823f6139debefdf105c032ae98ba31b33 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4602b16240f5bb7d2dcc502612f102d29e0170ca1552dc22b0e01fe47bdaf0a
|
|
| MD5 |
774f1a0efb6aad7d4e813c6f6d5f87b9
|
|
| BLAKE2b-256 |
0b3fcec04d75c27c2994b3957f303ee8ab098f7a7ce6c0894222a6b595d33957
|
Provenance
The following attestation bundles were made for scribesh-1.0.0-py3-none-any.whl:
Publisher:
release.yml on BearlyStable/scribe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
scribesh-1.0.0-py3-none-any.whl -
Subject digest:
d4602b16240f5bb7d2dcc502612f102d29e0170ca1552dc22b0e01fe47bdaf0a - Sigstore transparency entry: 2269132714
- Sigstore integration time:
-
Permalink:
BearlyStable/scribe@0072be7823f6139debefdf105c032ae98ba31b33 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/BearlyStable
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0072be7823f6139debefdf105c032ae98ba31b33 -
Trigger Event:
push
-
Statement type: