No description
  • Scheme 86.2%
  • Shell 7.5%
  • Makefile 3.4%
  • C 2.8%
Find a file
2026-07-30 15:39:17 -06:00
.builds ci: set umask 022 in package/verify tasks too (jerbuild creates group-writable cache dirs during pkg operations) 2026-07-22 13:10:58 -06:00
.jerboa Harden Signal release evidence privacy 2026-06-26 19:58:49 -06:00
docs Always save incoming attachments 2026-07-30 12:55:15 -06:00
scripts Harden attachment, log, and native handling 2026-07-11 17:26:08 -06:00
signal Always save incoming attachments 2026-07-30 12:55:15 -06:00
support Harden attachment, log, and native handling 2026-07-11 17:26:08 -06:00
tests Always save incoming attachments 2026-07-30 12:55:15 -06:00
.gitignore jpkg: complete binary package setup 2026-07-30 15:10:43 -06:00
.jerbuild Harden attachment, log, and native handling 2026-07-11 17:26:08 -06:00
.jpkgignore Update project configuration and documentation 2026-07-30 12:55:44 -06:00
AGENTS.md Update project configuration and documentation 2026-07-30 12:55:44 -06:00
handoff-corruption.md Update project configuration and documentation 2026-07-30 12:55:44 -06:00
jpkg.lock jpkg: complete binary package setup 2026-07-30 15:10:43 -06:00
jpkg.policy.sexp jpkg: complete binary package setup 2026-07-30 15:10:43 -06:00
jpkg.sexp jpkg: complete binary package setup 2026-07-30 15:10:43 -06:00
kimi3.md fix corruption 2026-07-23 18:38:57 -06:00
LICENSE Switch to MIT license 2026-07-21 13:42:19 -06:00
Makefile Update project configuration and documentation 2026-07-30 12:55:44 -06:00
no-blinky.md new AGENTS.md 2026-07-24 11:54:47 -06:00
README.md Harden attachment, log, and native handling 2026-07-11 17:26:08 -06:00
SECURITY.md Harden attachment, log, and native handling 2026-07-11 17:26:08 -06:00
signal.md Update project configuration and documentation 2026-07-30 12:55:44 -06:00

jerboa-signal

A Signal client written in Jerboa, driving signal-cli (Java) as a JSON-RPC subprocess. All orchestration, arg parsing, and message routing is Jerboa; the Signal protocol itself is delegated to signal-cli's libsignal-service-java.

What it does

Bidirectional bridge between Signal and your shell:

jerboa-signal send [-a +PHONE] RECIPIENT MESSAGE   # send one message, exit
jerboa-signal listen [-a +PHONE]                   # stream inbound as NDJSON
jerboa-signal tui [-a +PHONE]                      # terminal UI shell

-a is required only if multiple Signal accounts are linked to signal-cli.

Prerequisites

  • signal-cli 0.14.5 or newer on PATH (Homebrew may lag; the local scripts/signal-cli-jvm wrapper uses ~/.local/opt/signal-cli-0.14.5). Older 0.14.4.1 builds drop current sealed-sender receive envelopes with getServerGuid(...) must not be null. Release evidence currently reviews exactly signal-cli 0.14.5; a newer installed version must be reviewed before publication.
  • Jerboa build tools. make binary/make install use project-local ./jerbuild first, then .jerboa/bin, then PATH, and download the matching release artifact if none are available.

jerboa-signal expects signal-cli to already be linked. Run this once:

signal-cli link -n "jerboa-signal"

It prints a sgnl://linkdevice?uuid=... URI. On your phone: Signal → Settings → Linked devices → "+" → scan/paste the URI. signal-cli will register the device and exit. Verify:

signal-cli listAccounts

Usage

make install                                   # installs ~/.local/bin/jerboa-signal
jerboa-signal send +15551234567 "hello world"
jerboa-signal listen | jq .                    # pretty-print incoming envelopes
jerboa-signal --trace /tmp/jerboa-signal.trace tui

Without make install, run via the Makefile:

make run ARGS="send +15551234567 hello"
make run ARGS="listen"
make run-tui

The TUI needs the small termbox2 shim:

make tui-shim
./jerboa-signal tui

The TUI opens signal-cli jsonRpc --receive-mode manual, then subscribes with subscribeReceive once the app reader is active, so new Signal events arrive while it is running without racing the UI startup. At startup it preloads active groups and known contacts into the conversation pane. The current interface keeps in-memory conversations for this session, routes incoming messages, edits, reactions, deletes, calls, receipts, and typing events into threads, and sends plain text from the composer to the selected conversation with Enter. Use Up/Down to change the selected conversation. Use Ctrl-N to open the new-message picker, type to filter contacts/groups or enter a +number, and press Enter to select. Use Ctrl-F to search message history across all conversations at once: type a query, use Up/Down to move through matches, and press Enter to jump to the conversation holding that message. Search matches message text and sender, and covers everything received or sent during the current session — history is in-memory only, so it does not include messages from before the TUI was opened. Use Ctrl-D to delete the selected conversation; a y/n prompt confirms first. For a direct contact this calls signal-cli removeContact --forget, which erases all local Signal data for that contact (identity keys, sessions, profile) — use it to remove someone entirely. For a group it just drops the conversation from the list. Deleted conversations are remembered in ~/.local/share/jerboa-signal/removed-<account>.txt and stay hidden across relaunches, even though signal-cli (a linked device) keeps re-syncing your phone's contacts. If a removed person messages you again the thread reappears; to remove someone from your Signal account everywhere, delete them on your phone too. Use Ctrl-Q or Ctrl-C to quit.

Debug tracing

Runtime tracing is opt-in and writes a chmod 600 file:

jerboa-signal --trace /tmp/jerboa-signal.trace tui

The default trace records safe internal telemetry such as send phases, timings, byte counts, log backend selection, history replay counts, and encrypted log persistence stages. It does not include message bodies or raw Signal RPC frames.

For maximum debugging detail, including raw RPC frames and signal-cli verbose stderr, use:

jerboa-signal --trace-all /tmp/jerboa-signal.raw.trace tui

That file is sensitive: it can include message content and Signal identifiers. To force the encrypted log to be rewritten after every row while debugging durability issues, use:

jerboa-signal --trace-all /tmp/jerboa-signal.raw.trace --log-persist immediate tui

The default is batch: the encrypted container is rewritten after 64 rows, 1 MiB of new retained payload, or two seconds, whichever comes first, and at clean close. This avoids whole-database work for every message while bounding the crash window. --log-persist immediate forces a rewrite after every row; --log-persist close persists only at clean exit. Operators can lower the bounded batch thresholds with JERBOA_SIGNAL_LOG_BATCH_ROWS, JERBOA_SIGNAL_LOG_BATCH_BYTES, and JERBOA_SIGNAL_LOG_BATCH_MS.

Security

jerboa-signal is not a Signal protocol implementation. It delegates Signal cryptography, account state, server communication, and device linking to signal-cli. The local security boundary is the Jerboa orchestration layer: JSON-RPC framing, trace redaction, attachment export, encrypted local message logs, terminal UI FFI, and process execution.

Before using this on production accounts, read SECURITY.md, docs/threat-model.md, docs/credential-handling.md, and docs/ffi-boundary.md. Run make verify from a clean checkout and preserve make release-evidence output for release candidates. The release evidence includes SBOM/provenance, repeated binary reproducibility, a no-secret binary startup smoke, signal-cli advisory/version review, and explicit status fields for live Signal smokes that require a non-production linked account.

Encrypted message log

The TUI can log every message to an encrypted SQLite database so you keep a copy even when the sender later deletes it ("delete for everyone"). Logs use jsqlite for new logs and a Jerboa-native encrypted container around the database image; contents and metadata are unreadable without your passphrase. Existing SQLCipher logs are still opened through the legacy shim so old history continues to replay.

Enable it:

./jerboa-signal tui           # prompts: passphrase for encrypted message log

At startup the TUI asks for a passphrase on the controlling terminal. It fails closed if the exact terminal state cannot be saved, echo disabled, or the state restored. Enter one to log; leave it blank to skip logging for that session. For unattended use, prefer --log-key-fd FD with a dedicated inherited FD. JERBOA_SIGNAL_DB_KEY remains available but exposes the secret to the process environment.

The log lives at ~/.local/share/jerboa-signal/messages-<account>.db. Each inbound notification and outbound send is one append-only row (with the full raw JSON); remote-deletes, edits, reactions, calls, attachments, and other displayable events are logged as new rows and never overwrite the original.

Retention is bounded by default to 20,000 rows, 128 MiB of retained payload, and 4 MiB per entry; oldest rows are pruned before an insert exceeds the row or payload budget. The limits can be lowered with JERBOA_SIGNAL_LOG_MAX_ROWS, JERBOA_SIGNAL_LOG_MAX_PAYLOAD_BYTES, and JERBOA_SIGNAL_LOG_MAX_ENTRY_BYTES.

To convert an older SQLCipher log to the current jsqlite container format:

jerboa-signal migrate-log     # add -a +PHONE if you use a named account

The converter verifies the new row count before swapping files and keeps the old SQLCipher database beside it as messages-<account>.db.sqlcipher.bak.

Logging happens only while the TUI is open — messages that arrive while it is closed are not captured.

Architecture

┌─────────────────┐  argv      ┌──────────────────┐  NDJSON  ┌────────────┐
│ jerboa-signal   │ ─────────► │ signal/main.ss   │ ───────► │ signal-cli │
│   (CLI argv)    │            │  (subcommands)   │          │  jsonRpc   │
└─────────────────┘            └──────────────────┘ ◄─────── └────────────┘
                                        │           NDJSON          │
                                        ▼                           ▼
                                  stdout / stderr            Signal servers

Modules:

  • signal/main.ss — entry point, argv parsing, subcommand dispatch
  • signal/rpc.ss — JSON-RPC client over signal-cli's stdin/stdout (NDJSON framing, id correlation, notification queue)
  • signal/rpc-actor.ss — long-lived JSON-RPC actor for the TUI
  • signal/cmd-send.sssend subcommand
  • signal/cmd-listen.sslisten subcommand
  • signal/cmd-tui.ss — TUI command entry point; see docs/TUI_PLAN.md
  • signal/store.ss — persistent local state (deleted-conversation list)
  • signal/capture.ss — normalizes a notification into an encrypted log row
  • signal/logdb.ss + signal/log_crypto.ss — encrypted log backend
  • signal/tui/ — termbox2 FFI and the first terminal shell

License

Apache-2.0.