- Rust 99.3%
- Shell 0.6%
|
Some checks are pending
Binaries / package (aarch64, macos-latest, apple-darwin) (push) Waiting to run
Binaries / package (aarch64, ubuntu-latest, unknown-linux-gnu) (push) Waiting to run
Binaries / package (x86_64, macos-latest, apple-darwin) (push) Waiting to run
Binaries / package (x86_64, ubuntu-latest, unknown-linux-musl) (push) Waiting to run
Binaries / package (x86_64, windows-latest, pc-windows-msvc) (push) Waiting to run
CI / test (macos-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / Coverage report (push) Waiting to run
CI / Flake checks ❄️ (push) Waiting to run
CI / Flake checks ❄️-1 (push) Waiting to run
|
||
|---|---|---|
| .github | ||
| crates | ||
| docs | ||
| scripts | ||
| src | ||
| .envrc | ||
| .gitattributes | ||
| .gitignore | ||
| .rustfmt.toml | ||
| AGENTS.md | ||
| archive-issues.log | ||
| backlog-methodology.md | ||
| backlog.md | ||
| build.rs | ||
| build.sh | ||
| Cargo.lock | ||
| Cargo.toml | ||
| clippy.toml | ||
| config.example.toml | ||
| contain.sh | ||
| CONTRIBUTING.md | ||
| flake.lock | ||
| flake.nix | ||
| generic-prompt.txt | ||
| iamb.desktop | ||
| LICENSE | ||
| PACKAGING.md | ||
| README.md | ||
| ROADMAP.md | ||
| rust-toolchain.toml | ||
About
iamb is a terminal Matrix client with Vim-style interaction, a full-screen TUI, and a growing automation-oriented CLI.
It supports:
- E2EE, threads, spaces, replies, edits, reactions, read receipts, and mentions
- Room, DM, invite, unread, and session workflows from both the UI and CLI
- Archive/listen/download tooling for history, attachments, external media, hooks, and postback flows
- Custom keybindings, multiple profiles, image previews, image reactions, notifications, and configurable sorting
This repository is maintained at https://codeberg.org/microchipster/iamb.
Documentation
Prefer the built-in help first: iamb --help and iamb <command> --help stay closest to the code.
Use this README for project overview, installation, and a few higher-level workflows. Use docs/iamb.1 and docs/iamb.5 for concise man-page style references.
IPC and daemon model
iamb can reuse an authenticated Matrix client over IPC so CLI commands do not need to perform a fresh login or sync every time.
- The UI and
iamb daemonexpose a per-profile Unix socket unless--no-ipcis set. - CLI commands first try that socket; if a daemon is reachable, the command is executed there and reuses the warm session.
- If no daemon is available, the command falls back to local execution.
- Socket lookup order is
$XDG_RUNTIME_DIR/iamb/<profile>.sock, then$TMPDIR/iamb/<profile>.sock, then/tmp/iamb/<profile>.sock. --ipc-socket <PATH>overrides the resolved socket path for both client and server.
This makes one-off commands much faster in normal use and keeps helpfully stateful operations, such as room lookups and uploads, consistent with the running UI session.
Useful CLI workflows
The CLI is meant for scripting, maintenance, and headless usage, not just account bootstrap.
iamb rooms <query>fuzzy-searches joined rooms; use.to list all rooms and--user <USER>to require specific members.iamb rooms,iamb public-rooms,iamb users, andiamb statusall support--format jsonwhen you want a stable automation surface instead of text output.- Human-readable CLI reports now default to aligned tables and headings, while prompts stay off stdout so pipes and
--format jsonoutput remain machine-safe. If a command needs confirmation or a password, rerun it interactively or provide the value via flags/stdin when the command supports that path. iamb spaces <query>fuzzy-searches joined spaces;iamb spaces treeprints the joined space tree as text or JSON;iamb spaces organizeopens a staged CLI organizer for moving rooms between spaces orOrphans; andiamb spaces merge --suggestions/iamb spaces merge <source> [--into <dest>] [--apply]review or apply duplicate-space consolidation plans.iamb room info <room>prints room diagnostics;iamb room versionslists locally known rooms by version in TSV form;iamb room threads <room>lists thread roots;iamb room uploads <room>lists recent uploads.iamb dm @user:servercreates or reuses a direct message room, andiamb public-rooms <query>/iamb public-rooms --server <server> <query>/iamb public-rooms --known-servers <query>/iamb public-rooms --remembered-servers/iamb users complete <prefix>/iamb users discoverable <query>/iamb users shared <query>/iamb users rooms <@user:server>/iamb users members <!room:server>/iamb users whois <@user:server>expose directory and joined-room user queries without opening the UI. Useiamb users complete @hofor shell-friendly user-id suggestions when you only remember the start of an MXID or display name. Omit the discoverable-user query, or pass*/.*, to ask the homeserver for all discoverable users it is willing to return; if that endpoint returns nothing, iamb falls back to locally known shared-room members and reports that limitation. Passing a full MXID such as@user:serveralso tries the Matrix profile endpoint, which can verify exact users that are not prefix-discoverable. Explicit public-room--serverqueries are now remembered per profile, can be inspected without a network query, and are folded into later--known-serversruns. Shared-user queries now include cached presence when it is available locally, andusers roomsincludes per-room membership, power level, and a conservativecan_banpower-level check for roomless moderation triage.iamb inviteslists pending invites; add--accept,--reject, or--pickerfor bulk handling.iamb unreadslists unread rooms/messages and can clear them with public receipts, private receipts, or no receipt.unread_suppressionconfig rules can keep bridge/system noise visible in timelines without letting it mark rooms unread; mentions are preserved by default.iamb mark-unread <room-id>...marks rooms unread locally;--pickeropens an interactive selector.iamb send,iamb upload,iamb create-room,iamb room-setting,iamb upgrade,iamb leave,iamb sessions,iamb setup,iamb reset-secure-backup, andiamb change-passwordcover common operational tasks without opening the TUI. Piping text intoiamb send --room <room>sends that text as the message body when no explicit body, attachment, or reaction was provided; use--stdinwhen you want to force stdin or combine it with other inputs. Setmessage_send_format = "plain"or"html"to change the default send markup, while/markdown,/plain, and/htmlstill override one message. CLI reactions sent withiamb send --react-to ... --text smileaccept emoji shortcodes; add--reaction-literalto preserve an exact reaction key.iamb media,iamb archive,iamb listen,iamb download,iamb postback,iamb search, andiamb redactsupport archival, media, and cleanup workflows.
TUI quick reference
The interactive : command set is broader than the short examples in this README. The most useful discovery-oriented commands are:
Press <Tab> in the command bar to complete command names, common subcommands, Matrix IDs, room aliases, file paths, and emoji shortcodes in the main flows such as :invite send, :join, :room ..., :space child ..., :upload, and :react. Command and search history are now persisted per profile too, so pressing Up/Down in the command bar can recall recent entries even after restart. In the message composer, typing a room alias like #room:server now sends a matrix.to permalink, and typing # then <Tab> inserts a known room alias without leaving the keyboard.
:rooms,:chats, and:dmsaccept optional sort modes, while:sort <mode>changes the active list ordering; the footer shows the current sort label.:join <room>accepts room IDs, aliases, andmatrix.tolinks; use:peek <room>to inspect a room before switching into a joined timeline. Sharedmatrix.toroom/event links opened from messages now reuse the same internal join/navigation path instead of always launching a browser, and:openon Matrix location messages opens theirgeo:URI through the configured opener.:public-rooms [query]and:users discoverable [query] [limit]/:users shared [query]/:users rooms <@user:server>/:users members <!room:server>/:users whois <@user:server>expose the public-room and user-query flows inside the TUI too. The command bar completes known user IDs by MXID prefix or display name for:dm,:invite send,:kick <@user:server>,:users rooms,:users whois,:users shared, and room moderation/role commands. Shared-user output and member lists now surface cached presence when it is available locally, and:kickworks contextually from room scrollback or:memberswith confirmation while:users rooms/:users whoisinclude roomless moderation context for shared rooms.:infoopens the room report, and:pins/:pins open <n>let you browse pinned events and jump back into the timeline.:sourceopens an Element-style source view for the selected message, while:source PATHor:source save PATHwrites the current/decrypted source JSON to disk; use:source! ...to overwrite an existing file.:raw on/:raw offtemporarily forces the selected message to render as raw body text instead of the configured HTML/plain renderer.iamb message <room> <event-id>prints pretty event source JSON for scripts; add--out ./event.jsonto save it from the invoking CLI process, and--forceto overwrite an existing file.:markdown onenables one-message markdown authoring explicitly, while:markdown offreuses the plain-text send path for the next message only. Themessage_send_formatsetting changes the normal composer default tomarkdown,plain, orhtml; inline/markdown,/plain, and/htmlstill override one message. Inline code and fenced code blocks now use stronger code styling so snippets stay visible.:confettiand:snowfallnow match the existing slash-command effects for the next message, so you can trigger those Matrix client-side visuals from the:command bar too.:room show,:room avatar ...,:room encryption ...,:room retention ...,:room role ...,:room threshold ..., and:room admin showcover the room inspection and moderation surfaces that also exist underiamb room-setting, while:room mentions next|prev|previousand:room me last/:room own lastadd room-local navigation helpers in the focused room.:spaces!opens the hierarchical space tree,:space tree movestarts a staged move there, and:space tree review/:space tree applylet you inspect and commit the batch before mutating server state.:help,:drafts,:status,:log,:reload,:initial-sync,:setup,:reset-secure-backup, and:perfare the main recovery/inspection commands when the UI needs a nudge or a quick health check. Drafts survive restart, rooms with saved drafts are marked with📝in list views, and:draftsopens the current saved-draft list. Use:helpfor an in-client quick manual and:logto see the active debug log path or flush it before grabbing logs for support.- The
:help keyspage and scrollback views include Vim-style scroll placements such asz+andz^, which move the current line just outside the viewport.
If you need the full command inventory in one place, prefer docs/iamb.1 or man ./docs/iamb.1.
iamb create-room provides a straightforward way to spin up a room with name/topic, directory visibility, invites, encryption, guest access, and join/history rule tweaks without dropping into the UI. Example:
iamb create-room --name "Project Workspace" --topic "Planning notes" \
--visibility private --preset private-chat --invite @alice:example.com --invite @bob:example.com \
--encrypted --guest-access --join-rule invite --history-visibility shared
iamb upgrade <room> upgrades the named joined room to the latest stable Matrix room version advertised by the homeserver. The command automatically restores the saved session or reuses the IPC daemon client, verifies m.room.tombstone permissions, calls /rooms/<room>/upgrade, and prints the replacement room ID so you can immediately follow the successor chain (or fall back to :upgrade follow after the tombstone is emitted). Rooms already running the preferred version simply report their current version. In the interactive UI you can run :upgrade latest (alias :upgrade current) to perform the same flow, show an in-place InfoMessage, and jump into the upgraded room while leaving the old room tombstoned so others can follow with :upgrade follow. :upgrade follow now makes the expectation explicit: it switches to the replacement room immediately when you are already joined, or joins it first if needed. Use :upgrade old to view the old version of an upgraded room. iamb room versions now adds predecessor/successor status and action columns so you can review which upgrade relationships are already joined locally before deciding what to follow next, and --predecessor-action / --successor-action let you filter that review output down to the rooms that still need a specific follow action.
The CLI flags map directly to the Matrix room creation API (preset, visibility, room_alias_name, join_rule, history_visibility, initial_state). Any of those settings can later be adjusted via iamb room-setting <room-id>.
Shell completions
iamb now ships with generated completions for every flag and subcommand. The scripts are emitted from the live clap command tree, so they track the real CLI surface instead of a hand-maintained copy. Install them by generating the script and dropping it in the right directory for your shell:
- bash:
mkdir -p ~/.config/iamb && iamb --completions bash > ~/.config/iamb/iamb.bash-completion. Addsource ~/.config/iamb/iamb.bash-completionto your~/.bashrcor drop the file into/etc/bash_completion.d/if you prefer a system-wide install. - zsh:
mkdir -p ~/.config/iamb && iamb --completions zsh > ~/.config/iamb/_iamb. Ensure~/.config/iambis on yourfpath(e.g.,fpath=(~/.config/iamb $fpath)inside~/.zshrc), then runautoload -U compinit && compinit(or restart the shell) so the new completion becomes available. - fish:
mkdir -p ~/.config/fish/completions && iamb --completions fish > ~/.config/fish/completions/iamb.fish. The shell auto-loads~/.config/fish/completions/*.fish, so either restart orsource ~/.config/fish/completions/iamb.fish.
Regenerate the outputs (iamb --completions <shell>) whenever the CLI gains new flags or subcommands.
Development Coverage
Use scripts/coverage.sh for quick local coverage summaries while you are iterating, and scripts/coverage-report.sh for the routine artifact bundle that CI and ./build.sh now publish locally.
Quick local summary:
scripts/coverage.sh
scripts/coverage.sh --package iamb-core
scripts/coverage.sh --html
The default run covers deterministic workspace tests only. Ignored real-server tests are included only when explicitly requested with scripts/coverage.sh --include-real-server, using IAMB_TEST_CONFIG or ~/.config/iamb/test.toml.
Routine artifact bundle:
scripts/coverage-report.sh
scripts/coverage-report.sh --package iamb-core
scripts/coverage-report.sh --include-real-server
That routine path writes target/llvm-cov/summary.json, summary.md, gap-audit.md, lcov.info, and an inspectable HTML report under target/llvm-cov/html/. The default mode is explicitly deterministic-only. Real-server coverage remains opt-in and separately labeled.
GitHub Actions now also runs the deterministic scripts/coverage-report.sh path on Ubuntu and uploads the resulting artifact bundle for review, keeping coverage visible without turning it into a hard percentage gate.
./build.sh now runs that same deterministic coverage-report path by default before the release-profile test lane. Use ./build.sh --skip-coverage-tests or IAMB_SKIP_COVERAGE_TESTS=1 ./build.sh only when you intentionally want a faster local bypass.
Change-Aware Local Test Selection
Use scripts/test-changes.sh for a conservative local test suggestion based on changed paths. It is intentionally fail-open: docs-only changes select no cargo test command, package-scoped crate changes widen to known downstream packages, shared/tooling/root changes widen to cargo test --workspace --locked or all the way to ./build.sh.
Examples:
scripts/test-changes.sh --dry-run
scripts/test-changes.sh --base origin/main --dry-run
scripts/test-changes.sh --path crates/iamb-tui/src/main_application.rs --dry-run
This helper is only for fast local confidence while iterating. ./build.sh remains the canonical broad verification path before merge-quality handoff.
Real-Server Tests
Use scripts/test-real-server.sh to run opt-in integration tests against a real Matrix homeserver with normal test users. Add a local [test_server] section in ~/.config/iamb/test.toml, or set IAMB_TEST_CONFIG=/path/to/config.toml. Password environment variables and device names are inferred from each user key, such as alice -> IAMB_TEST_ALICE_PASSWORD and iamb-test-alice; password_env, device_name, and a direct local-only password remain optional overrides. Environment variables take precedence over direct passwords when both are present. A valid test.toml is enough to opt into real-server tests; the wrapper exports IAMB_TEST_SERVER=1 for Cargo. Admin reset tests are intentionally separate and require IAMB_TEST_ADMIN=1 plus IAMB_TEST_ADMIN_TOKEN.
Configuration
You can create a basic configuration in $CONFIG_DIR/iamb/config.toml that looks like:
[profiles."example.com"]
user_id = "@user:example.com"
If you homeserver is located on a different domain than the server part of the
user_id and you don't have a /.well-known entry, then
you can explicitly specify the homeserver URL to use:
[profiles."example.com"]
url = "https://example.com"
user_id = "@user:example.com"
If a homeserver exposes Matrix endpoints on a different host than the web UI you
use to sign in (for example, chat.fedoraproject.org versus the m.homeserver
base_url of https://fedoraproject.ems.host, or utwente.io versus
https://matrix.utwente.io), retrieve /.well-known/matrix/client yourself and
set profiles."name".url to the reported m.homeserver.base_url. The CLI
relies on that explicit url to know which host actually answers the Matrix
API, so pointing it at the shiny UI host will forever hand the SDK 404 HTML
errors instead of JSON if the API lives somewhere else.
To automatically log in without interactive prompts, point password_file at a
local file containing the account password (the file is read every startup). Keep
the file permission-restricted so only the owning user can read it.
Startup windows and layout
Startup behavior comes from the layout section plus settings.default_room:
layout.style = "restore"restores the last saved layoutlayout.style = "new"openssettings.default_roomwhen set, otherwise the welcome pagelayout.style = "config"opens the exact tabs and splits you describe under[[layout.tabs]]
settings.default_room can point at a room, alias, DM user, or a special iamb
window URI such as iamb://welcome, iamb://rooms, iamb://chats, or iamb://dms.
If the configured target cannot be opened, iamb falls back to the welcome page.
Split defaults
If you prefer cursor-file opens to use a vertical split, set:
[settings]
default_split = "vertical"
That changes <C-W>f and <C-W><C-F> from the default horizontal split to a vertical one.
To inspect the fully resolved configuration that the active profile is actually using, run iamb config current --format json or iamb config current --format toml.
Extra sender lines
If long sender names are making the room gutter feel cramped, you can move senders onto their own line above the message body:
[settings]
message_sender_extra_line = true
That gives the message body the full terminal width and makes narrow or long-name rooms easier to scan without shrinking the main text area.
Raw message bodies
If you want to see the raw Matrix message body instead of rendered formatted_body HTML, disable rich message rendering:
[settings]
message_html_display = false
This keeps the underlying event content intact but shows the literal body text in scrollback, thread previews, and other message views.
Watched keywords
If you want specific words or phrases to stand out in message bodies, add them to a watched keyword list:
[settings]
watched_keywords = ["matrix", "urgent"]
The keywords are highlighted case-insensitively wherever message bodies are rendered, and you can set them either globally or on a per-profile basis.
Example fixed startup layout:
[settings]
default_room = "iamb://welcome"
[layout]
style = "config"
[[layout.tabs]]
window = "iamb://rooms"
[[layout.tabs]]
window = "iamb://chats"
[[layout.tabs]]
window = "iamb://welcome"
[[layout.tabs]]
split = [
{ window = "#iamb-users:0x.badd.cafe" },
{ window = "#iamb-dev:0x.badd.cafe" },
]
Low-memory profile
For very large or high-activity accounts, iamb now performs much better than it used to, but you can still trade some cache depth and prefetching for lower RAM usage.
The most useful knobs are:
sliding_sync: keep enabled unless you are debugging sync behaviorsliding_sync_active_timeline_limit: lower the active-room timeline depthsliding_sync_all_timeline_limit: lower the background-room timeline depthevent_cache: keep enabled unless you are isolating event-cache problemsevent_cache_clear_interval: lower values clear cached event data more often, in minutes after an initial warmup period capped at 5 minuteslog_rotate_size: cap each active log file before rotating it, using values like1Gor100Mlog_rotate_policy: choose whether rotated logs are kept in full (backup), kept as a warning/error-focused minified backup (backup-minified), or discarded (delete)prefetch_recent_minutes: lower values reduce how much recent history is prefetchedkey_sequence_timeout_ms: resolve ambiguous multi-key bindings after this many milliseconds; set to0to keep waiting indefinitely
Example low-memory profile:
[settings]
sliding_sync = true
sliding_sync_active_timeline_limit = 10
sliding_sync_all_timeline_limit = 2
event_cache = true
event_cache_clear_interval = 25
prefetch_recent_minutes = 30
Tradeoffs:
- lower
event_cache_clear_intervalcan reduce memory growth, but may increase cache churn because the SDK event cache is cleared after at most 5 minutes, then again on that many minutes of uptime - lower sliding-sync timeline limits reduce cached per-room timeline depth, which helps on large accounts but may slightly reduce how much recent history is instantly warm
- lower
prefetch_recent_minutescan reduce RAM significantly on busy accounts, but may make upward scrolling or unread catch-up feel a bit less warm - disabling
sliding_syncorevent_cacheis not recommended as a first step; prefer tuning the two values above first
Responsiveness checks
For large or busy accounts, use :perf in the TUI after a burst of activity to inspect the current
responsiveness snapshot. The report includes p50/p95/p99 input-to-draw latency, frame timing,
action/lock timing, worker-event counters, and recent slow events.
If a room report or pinned-event view looks incomplete after first opening it, keep the room focused for a moment or run :initial-sync to force a catch-up pass.
Empty room recovery
If a room suddenly shows up as Empty Room after an upgrade or stale sync, iamb usually is not missing a dedicated feature; it is waiting for fresh room state to arrive again.
Practical recovery steps, in order:
- keep the room focused briefly, then try
:initial-syncto request a fresh catch-up pass for the current view - if the UI still looks stale, run
:reloadto reload configuration and related runtime state; invalid config is now rejected instead of being normalized, so fix any validation error first, then revisit the room - if you can safely do so, sending a new message in the room often forces the room to become visibly active again
- profile changes made from another Matrix client, such as updating your display name or avatar, can also cause many rooms to refresh their visible state
Important caveats:
- iamb does not currently promise a one-shot “repair this room” button for every stale-room case, so do not assume
:reloador:initial-synccan reconstruct history the homeserver has not resent yet - avoid manually deleting iamb profile data or cache directories unless you understand the impact on local state and encryption material; export keys first with
iamb keys export ...if you are considering destructive cleanup :retrydecrypt/:clearcachehelp with undecryptable-message recovery, not general room-state resync
When validating responsiveness-sensitive changes locally, a good stress pass is:
- let sync settle on a large account for 30-60 seconds
- switch between busy rooms and list views
- scroll media-rich history upward quickly
- send a text message plus a file or clipboard image upload
- run
:perfand confirm p95 input->draw stays under 50ms and dropped worker events stay at 0
Archival utilities
listen
iamb listen streams timeline events into an archive directory suitable for later processing and inspection. It writes per-room NDJSON event files under events/, stores downloaded attachments and cached assets under media/, and keeps bookkeeping and job metadata in meta/ (for example synced markers, postback and failed URL logs). The listener collects in-room attachments and classifies external URLs, queuing jobs to fetch external media when appropriate.
Use --only-print-changes when you want long-running listener output to stay focused on new events and material activity while still showing warnings, errors, and the final summary.
External failures are appended to meta/failed-urls.ndjson; successful downloads that should be posted back are queued in meta/postback-queue.ndjson.
Use iamb media-archive-output-path <event-id> --out ./archive to ask iamb for the canonical path(s) where media from an archived event should live. Add --room <room-id> to avoid scanning every event log, or --url <url> to print only the target path for one external URL in that event. This is intended for out-of-tree download helpers that want to place files in iamb's archive layout without adding downloader policy to iamb itself.
Usage example:
cargo run --bin iamb -- -P default listen --out ./archive
Custom bot commands (--command)
iamb listen can hand off each new message to an external helper by passing --command <program> alongside the rest of the listener invocation. The listener creates a small bot workspace inside the archive root (meta/bot/) and invokes your program like this for every decoded burst:
<program> <thread_id> <archive-root>/meta/bot/prompt.ndjson \
--attachment <archive-root>/media/<room>/<file> --attachment <archive-root>/media/<room>/<file2> ...
thread_id is a deterministic alphanumeric identifier derived from the room plus the thread root event, so your helper can keep per-thread state in sync while it receives just the latest prompt. --prompt points at meta/bot/prompt.ndjson, which is newline-delimited JSON because the listener now waits roughly 2 seconds after the first event before invoking your helper. This allows quick sequences (text after an image, multiple replies, etc.) to be bundled into one invocation. The shape of each entry is the same as before:
You can also pass --exclude-room <room_id> (repeatable) to keep specific rooms out of the listener and any bot invocations; persist the same list via archive.listener.excluded_rooms in your profile when you need the exclusion to survive across runs. Use --accept-invite-whitelist/--accept-invite-blacklist together with the archive.listener.invites.whitelist/archive.listener.invites.blacklist config arrays to teach the listener which invites should be accepted (room IDs and display names are matched against the supplied regexes).
{
"room_id": "!room:example.com",
"event_id": "$event:example.com",
"sender": "@alice:example.com",
"type": "m.room.message",
"origin_server_ts": 1700000000000,
"thread_root": "$thread:example.com",
"content": {
"msgtype": "m.text",
"body": "Hello world"
}
}
thread_root now always contains the root event that the conversation is happening in (it is only null for the very first message, which also becomes the thread root). The history files live under meta/bot/history and are named after the room and the percent-encoded thread root (<room>__main.ndjson vs <room>__thread__<event>.ndjson). Each history file is newline-delimited JSON that mixes previous incoming events and the replies that the helper already sent; the listener keeps that file trimmed to the most recent 512 lines so you can treat it as a rolling conversational context.
When a Matrix attachment is part of the batch, the helper receives one --attachment argument for each downloaded file (e.g. media/<room>/<file>). Any caption on that attachment already appears in the matching JSON entry's content.body; if there is no caption, the JSON line may be empty while the file is still supplied via --attachment. Your helper should parse every line in --prompt (it can contain multiple events) and inspect every --attachment path it receives, then write the text that should be sent back. Empty stdout or a non-zero exit code suppresses a reply, so send logs and diagnostics to stderr.
The snippet below illustrates a tiny standalone helper that reads the NDJSON prompt, enumerates the rolling history, reports the attachment paths, and prints a brief summary. It does not need to depend on the rest of iamb and can live in its own crate.
use clap::Parser;
use serde::Deserialize;
use std::{fs, path::PathBuf};
#[derive(Parser)]
struct Args {
#[arg(long)]
prompt: PathBuf,
#[arg(long)]
history: PathBuf,
#[arg(long)]
attachments: Vec<PathBuf>,
}
#[derive(Deserialize)]
struct EventContent {
msgtype: String,
body: String,
}
#[derive(Deserialize)]
struct EventLine {
room_id: String,
event_id: String,
sender: String,
#[serde(rename = "type")]
kind: String,
origin_server_ts: u64,
thread_root: Option<String>,
content: EventContent,
}
fn read_history(path: &PathBuf) -> Vec<EventLine> {
fs::read_to_string(path)
.ok()
.into_iter()
.flat_map(|text| text.lines().map(|line| serde_json::from_str(line)))
.filter_map(Result::ok)
.collect()
}
fn read_prompt(path: &PathBuf) -> Vec<EventLine> {
fs::read_to_string(path)
.ok()
.into_iter()
.flat_map(|text| text.lines().map(|line| serde_json::from_str(line)))
.filter_map(Result::ok)
.collect()
}
fn summarize_prompt(lines: &[EventLine]) -> String {
if lines.is_empty() {
"no recent events".to_string()
} else {
lines
.iter()
.map(|entry| format!("{}: {}", entry.sender, entry.content.body.trim()))
.collect::<Vec<_>>()
.join(" ⇢ ")
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let prompt = read_prompt(&args.prompt);
let history = read_history(&args.history);
let summary = summarize_prompt(&prompt);
let attachments = args
.attachments
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
println!(
"Prompt {} entries · history {} entries · attachments [{}]\nSummary: {}",
prompt.len(),
history.len(),
attachments,
summary
);
Ok(())
}
Build this helper with cargo build --bin bot (or your preferred toolchain) and pass the resulting binary to iamb listen --command. Every time the listener encounters a fresh burst, the helper sees the NDJSON prompt, can reuse the rolling history for context, and prints whatever text should be sent back.
archive
iamb archive converts or bundles an existing event log into final artefacts such as HTML/Markdown exports or static copies. It consumes the files produced by iamb listen (notably the events/, media/, and meta/ hierarchies), and will read postback queue entries or hooks if present so cached external assets can be included or referenced in the generated outputs.
Use --only-print-changes when you want quieter archive runs that suppress unchanged-room chatter while still reporting real updates, warnings, errors, and the final summary.
Use --continue to resume from the newest archived event recorded for each room, rather than replaying the entire history from scratch.
Handling encrypted events
If iamb archive encounters events it still cannot decrypt after a handful of retries, it now stops with an error, lists the affected rooms, and records each event ID under meta/undecryptable.txt. The undecryptable ledger is maintained automatically by the tools: entries will be added when events cannot be decrypted and will be removed automatically once keys become available.
Archive now persists a little more context for each unresolved event in that ledger, including repeated archive-run sightings and any sender / session / withheld metadata present on the raw encrypted event. Final archive errors also print a more human-readable summary per event, including room display names, sender, session/device hints, and whether the gap is still considered retryable or has become a conservative likely permanent classification.
To unblock the run you can:
- Export keys from another verified client (
iamb keys export /path/to/keys mypass), copy the file, and import them on the archive machine (iamb keys import /path/to/keys mypass). - Re-run
iamb archiveonce the keys are present. - If the room or event should be skipped, prefer using the user-maintained ignore list in
meta/ignored-events.txt. This file accepts one event ID per line and may optionally include the room id and/or a friendly label after the event id, for example:
$EVENTID # just the event
$EVENTID !roomid:example.com # event + room
$EVENTID !roomid:example.com A friendly name
- If an entire room or homeserver should be treated as an acceptable permanent hole, prefer
meta/allowed-decryption-errors.txtover ignoring individual event IDs. That file accepts a room id or server name, plus an optional cutoff timestamp and label, using the same format as the other room-rule files. - If you rerun
iamb archive --continue, long-lived undecryptable events can eventually be auto-classified aslikely permanentand the archive will continue past them instead of blocking forever. The current conservative thresholds are 5 archive runs in general, or 3 archive runs when the raw encrypted event already carries withheld metadata.
Notes:
meta/undecryptable.txtis managed by the archiver/listener and should not be edited by hand except for advanced use cases. The tools will remove entries frommeta/undecryptable.txtonce the missing keys are available and events can be decrypted.- Events explicitly listed in
meta/ignored-events.txtwill remain skipped even after keys are imported; use the ignored-events list to permanently suppress specific events from processing. likely permanentis a heuristic, not certainty. iamb can report the sender device that originally created an encrypted event, but it cannot prove whether your missing room key copy lived only on a device that has already been logged out of or lost.
The live listener (iamb listen) never blocks on encrypted events, but it warns in the log and appends each unresolved event to the same meta/undecryptable.txt file so you know which rooms still need attention.
Decryption diagnostics
Pass the global --log-decrypt-errors flag when running the interactive UI or any CLI command that receives messages (the default launcher, iamb archive, iamb listen, and iamb unreads). When enabled, every event the client still cannot decrypt is appended as a newline-delimited JSON record to ~/.local/share/iamb/profiles/<profile>/encryption-errors.log. Each entry captures the room/event identifiers, sender, encryption metadata (algorithm/session_id/sender_key/device_id/withheld), the stage at which it was observed, and the full raw payload so you can investigate missing keys or bridging issues.
Retention cleanup (iamb redact)
iamb redact executes explicitly configured retention/cleanup policies from your profile's delete section. It does not create new policies from CLI flags — the flags only select which pre-defined policies to apply or temporarily override specific policy fields for that run.
How policy resolution works
- The effective policy for a given room and store is resolved in this order (later items override earlier ones):
- Global store defaults:
delete.stores.<Store>(lowest precedence) - Named room sets:
delete.room_sets.<Name>(applied in deterministic order) - Per-room overrides:
delete.rooms."<room_id>" - Top-level explicit excludes:
delete.exclude_rooms(these opt rooms out of global store defaults)
- Global store defaults:
- There are two common store kinds:
remote— operations that redact events on the homeserver (SDK redactions).archive— operations that edit the local on-disk archive (events NDJSON and local media files).
- If a policy requires archive presence (
require_archive_presence), the archive index (theevents/files) is consulted to decide whether a remote deletion should be applied to a particular event. - Candidate discovery defaults to live Matrix history; pass
--candidate-source archivewhen you wantiamb redactto seed the plan from the local archive NDJSON instead. - If you want a copy-and-edit replay artifact, add
--save-script path/to/redact.shand review the generated bash script. Each selected event gets its own executableiamb redactcommand, with the surrounding metadata and body preview kept as comments. - To narrow a generated or replayed plan to explicit event IDs, pass repeatable
--event <event_id>arguments.
Minimal example configuration
[delete]
allow_cli = true
default_rooms = ["!cleanup:matrix.org"]
# Global remote (server-side) policy
[delete.stores.remote]
enabled = true
include_media = true # include non-message media events as candidates
skip_peer_events = true # only affect messages sent by the profile user
older_than = "30d" # target events older than 30 days
# Global archive (local) policy — operate on the archive files & media
[delete.stores.archive]
enabled = true
include_media = true
older_than = "30d"
Both remote and archive can be configured independently; per-room overrides are used when you need a different retention rule for a particular room.
Practical scenarios and configuration hints
- Cleaning old data from the server
- Goal: redact messages older than N days from the remote homeserver.
- Config: set
delete.stores.remote.enabled = trueandolder_than = "30d"(or desired window). - Run: iamb -P profile redact --remote --dry-run
- Once satisfied, re-run without --dry-run to perform remote redactions.
- Purging something you didn’t want saved anywhere (remote + archive)
- Goal: remove events both from the server and your local archive.
- Config: enable and tune both stores (archive + remote) with matching
older_thanandinclude_mediaas needed. - Run: iamb -P profile redact --remote --archive --dry-run
- Confirm and re-run without --dry-run to redact remotely and prune local NDJSON + media files.
- Configuring disappearing messages for a room
- Goal: make a room automatically cleaned up after a time window.
- Config: add a per-room entry under
delete.rooms."<room_id>".stores.<Store>and setolder_thanandenabled = true. Optionally setleave_empty_roomsif you want the client to leave/forget empty rooms. - Use
delete.room_setsto apply the same disappearing rule to many rooms.
- Automating periodic cleanup (cron)
- Put a safe cron job that runs with
--dry-runfirst when testing, then without once validated. - Example cron invocation (monthly run):
- 0 3 1 * * /usr/bin/iamb -P myprofile redact --archive --remote --dry-run >> /var/log/iamb-delete.log 2>&1
- After validating outputs, remove
--dry-run.
- Fully purging a room (include leave + media removal)
- Goal: remove archived events, delete associated media files, optionally leave the room when empty.
- Config: per-room
delete.rooms."<room_id>".stores.archivewithenabled = true,include_media = true, andleave_empty_rooms = true. - Run: iamb -P profile redact --archive --dry-run (inspect) then re-run without --dry-run.
Important operational notes
-
CLI flags like
--older-thanand--mediatemporarily override policy fields for that invocation only; they do not create or persist a policy. To make changes persistent, edit your config. -
Start with
--dry-runevery time and inspect results carefully. -
Inspect resolved policies before running destructive operations: iamb -P profile config current --format toml This prints the resolved configuration (including merged defaults/overrides) so you can confirm what will be applied.
-
If you rely on
archivechecks (require_archive_presenceorarchive_before) ensure the archive root exists and is readable by the process that runsiamb redact. -
Faster defaults are always active; the archiver keeps
meta/room-sync.jsonup to date with the last archived event per room and skips rooms whose remote history has not advanced since the previous run. Useiamb archive --full(or delete that metadata file) to force a complete rescan if you suspect new events were missed, then tunearchive.archiver.fetch_limit,archive.archiver.message_fetch_retry_delay_secs,archive.archiver.message_fetch_timeout_secs,archive.listener.heartbeat_interval_secs,archive.listener.idle_sleep_millis, and thearchive.archiver.*/archive.listener.*external rate-limit entries when you need more conservative pacing. Seedocs/addressed/notes/archiver.mdfor guidance. -
Keep rooms out of the listener by configuring
archive.listener.excluded_roomsin your profile; the same list can be supplied temporarily using--exclude-roomon the CLI. You can also usearchive.listener.invites.whitelist/archive.listener.invites.blacklist(or the CLI equivalents) to control which invites are automatically accepted.
Sender and homeserver-aware cleanups
You can layer the --sender, --homeserver, and --homeserver-other flags on your remote policy runs to only target specific users or domains. Keep named room sets handy so you can easily point a cleanup at public rooms, encrypted rooms, or other slices of your archive.
[delete.room_sets.public]
rooms = ["!public1:example.com", "!public2:example.com"]
[delete.room_sets.unencrypted]
rooms = ["!open1:example.com", "!open2:example.com"]
[delete.room_sets.encrypted]
rooms = ["!secret1:example.com", "!secret2:example.com"]
-
Redact only your own messages in the public room set between 3 and 12 months old:
iamb -P public redact --remote --sender @user:example.com --older-than "90d" --newer-than "365d"
--room !public1:example.com --room !public2:example.com --dry-run
2. Target non-encrypted rooms only when removing your own activity from the last 30–90 days:
```bash
iamb -P public redact --remote --sender @user:example.com --older-than "30d" --newer-than "90d" \
--room !open1:example.com --room !open2:example.com --dry-run
-
Repeat the same clean-up for encrypted rooms that match your
delete.room_sets.encryptedlist:
iamb -P public redact --remote --sender @user:example.com --older-than "30d" --newer-than "90d"
--room !secret1:example.com --room !secret2:example.com --dry-run
4. Redact every event originating from a specific home server (or servers) regardless of the sender:
```bash
iamb -P public redact --remote --homeserver matrix.org --homeserver example.org --older-than "180d" --newer-than "720d" --dry-run
-
Clean up content from homeservers that are not your own (useful when bridging or roaming):
iamb -P public redact --remote --homeserver-other --older-than "90d" --newer-than "365d" --dry-run
These combinations give you the expressive filter power to redact messages by sender, room type, or server while still respecting your configured retention policies.
### mentions
By default, `@all` and `@channel` will send mentions to every member of the room (@room is specially handled by some clients so it is skipped). To opt out, set:
```toml
[settings]
expand_everyone_mentions = false
room-space context in lists
If you want room, chat, DM, and invite lists to show the parent space for each room, enable:
[settings]
show_room_space_context = true
This is off by default so compact list views stay unchanged unless you explicitly opt in.
verify
iamb verify logs in and runs a guided self-verification flow on the command line in isolation.
Inside the TUI:
:verifyopens the active SAS verification list.:verify loginstarts the same guided self-verification flow for the current session.
During a fresh interactive login, iamb now requires verification before finishing bootstrap. You can choose a recovery/security key, another signed-in device, or both. If verification does not complete, iamb falls back to the secure-backup reset flow so the new login can become trusted.
If the login is fresh and secure backup is not yet configured, iamb can also offer setup on the CLI or :setup in the TUI. That path bootstraps secure backup on a trusted device without asking for a local room-key export first, so the first-login flow stays explainable instead of failing on a missing export path.
setup
Bootstrap secure backup on a freshly trusted device without exporting local room keys first. This is the explainable first-login path when you have just logged in and want iamb to set up recovery for you immediately.
iamb -P profilename setup --password <pw>
:setup
What it does
- Restores the session if available; otherwise prompts for password or SSO, then performs an initial sync so encryption state is current.
- Generates or accepts a recovery passphrase, prompts you to retype it unless
--yesis set, and enables secure backup on the trusted device without requiring a local room-key export. - Prints the recovery key and passphrase; store them immediately in a password manager or secure medium.
reset-secure-backup
Enable or rotate secure backup on a trusted device and generate a fresh recovery passphrase and recovery key. Before any server-side change, iamb requires a local encrypted room-key export as a rollback copy unless you intentionally use --skip-keys-export for a trusted first-login bootstrap or emergency recovery.
If you only want a passphrase to stash first and use later, you can generate one without touching server state:
iamb -P profilename reset-secure-backup --generate-passphrase-only
iamb -P profilename reset-secure-backup
What it does
- Restores the session if available; otherwise prompts for password or SSO, then performs an initial sync so encryption state is current.
- Requires
--keys-export <path>and--keys-export-passphrase <value>(unless--generate-passphrase-onlyis used) and writes a local encrypted room-key export before changing recovery state. - Use
--skip-keys-exportonly when you intentionally want the trusted-device bootstrap path without exporting local room keys first. - Generates a high-entropy passphrase (you can supply
--passphrase <value>), prompts you to retype it unless--yesis set, then either:- enables secure backup via the SDK’s
recovery.enable().wait_for_backups_to_upload()flow when recovery is not enabled yet, or - rotates the recovery key via
recovery.reset_key()when recovery is already enabled and--forceis supplied.
- enables secure backup via the SDK’s
- Prints both the recovery key and passphrase; store them immediately in a password manager or secure medium.
What does not change
- This device keeps access to encrypted messages/media it can already read.
- Other already signed-in devices keep access to encrypted messages/media they can already read.
- No room history, messages, or media are deleted.
What does change
- New devices must use the new recovery key or passphrase after rotation.
- The old recovery key/passphrase should be treated as obsolete once the new one works.
Flags
--passphrase <string>— supply your own passphrase instead of auto-generating one.--keys-export <path>— required local encrypted room-key export path before changing secure backup.--keys-export-passphrase <string>— passphrase used to encrypt the required local room-key export.--generate-passphrase-only— print a secure-backup passphrase and exit without logging in or changing any backup state.--skip-keys-export— skip the local key export before changing secure backup; use only for trusted first-login bootstrap or emergency recovery.--force— recreate recovery even if already enabled.--yes— skip the confirmation prompt (non-interactive / automation).--password <pw>,--sso— optional login inputs, same asiamb verify.--timeout <secs>— limit how long login and verification can take before the command gives up.
Hooks, postback, and media configuration
External media jobs are classified and processed according to the MediaHandling configuration in your config.toml. Two related configuration areas control behavior:
-
media.hooks— fine-grained enable/disable and include/exclude pattern rules for each handler, either globally (media.hooks.default) or per-room (media.hooks.rooms). Rules are simple substring matches: anincludelist requires a substring to appear, andexcludeprevents matches containing a substring. Rules also have anenabledflag to turn handlers on/off for matching URLs. -
media.post_back— controls which fetched assets should be queued for postback. Legacy list-style options (defaultandrooms) continue to work, but you can now specifydefault_rulesandroom_rulesto apply the same pattern-based controls used bymedia.hooks.
A minimal example config.toml excerpt that demonstrates enabling the single-file browser, restricting yt-dlp globally to example.com links, and enabling postback only for a special room:
[media]
single_file_browser = "/usr/bin/chromium"
# Hook rules: enable yt-dlp only when the URL contains "example.com"
[media.hooks.default]
# Handler keys map to MediaHandlingKind names (Direct, YtDlp, SingleFile, MatrixAttachment)
YtDlp = { enabled = true, include = ["example.com"], exclude = [] }
Direct = { enabled = true }
# Postback: legacy list is still supported, but you can use rule-based controls
[media.post_back]
default = [] # no legacy global postback
# Enable postback for yt-dlp only in a specific room via room_rules
[media.post_back.room_rules."!special:example.com".YtDlp]
enabled = true
include = []
exclude = []
Note: enabling the optional Cargo feature yt-dlp-python allows iamb to query yt-dlp's Python extractors at runtime to determine whether yt-dlp can handle particular URLs. This can improve handler classification for many streaming or platform URLs. Enable it by building with --features "yt-dlp-python".
Command prefixes in messages (for example !yt-dlp ...) can still be used to force YtDlp handling for a URL. Fetched assets now land under the canonical per-room media directory media/<sanitized-room>/, with filenames that include the handler label (for example 2024-01-01@12:00:00__alice__yt-dlp__some-title.mp4). Hook scripts or automation can consume meta/postback-queue.ndjson to act on queued items (for example sending follow-up messages that include cached assets).
Example workflows
# setup
mkdir -p ~/.config/iamb
$EDITOR ~/.config/iamb/config.toml
# authenticate once and complete guided verification if needed
iamb -P profilename verify
# run a reusable background session for fast CLI commands
iamb -P profilename daemon
# archive everything once
iamb -P profilename archive --verbose --out ~/.local/share/iamb-archive/profilename
# continue archiving from the newest saved event per room
iamb -P profilename archive --verbose --out ~/.local/share/iamb-archive/profilename --continue
# listen only for selected rooms
iamb -P profilename listen --verbose --out ~/.local/share/iamb-archive/profilename \
--room <room-id0> --room <room-id1>
# clear unread markers without sending public receipts
iamb -P profilename unreads clear --no-receipt --all
Installation (from source)
Install Rust and Cargo using rustup, and then run from the directory containing the sources (ie: from a git clone):
cargo install --locked --path .
Installation (via crates.io)
Install Rust (1.83.0 or above) and Cargo, and then run:
cargo install --locked iamb
See Configuration for getting a profile set up.
Installation (via package managers)
Arch Linux
On Arch Linux a package is available in the Arch User Repositories (AUR). To install it simply run with your favorite AUR helper:
paru iamb-git
FreeBSD
On FreeBSD a package is available from the official repositories. To install it simply run:
pkg install iamb
Gentoo
On Gentoo, an ebuild is available from the community-managed GURU overlay.
You can enable the GURU overlay with:
eselect repository enable guru
emerge --sync guru
And then install iamb with:
emerge --ask iamb
macOS
On macOS a package is available in Homebrew's repository. To install it simply run:
brew install iamb
NetBSD
On NetBSD a package is available from the official repositories. To install it simply run:
pkgin install iamb
Conda / Pixi
On platforms supported by conda-forge, iamb can be installed with Conda-compatible tools:
conda install conda-forge::iamb
Or with pixi:
pixi global install iamb --channel conda-forge
Nix / NixOS (flake)
nix profile install "git+https://codeberg.org/microchipster/iamb.git"
openSUSE Tumbleweed
On openSUSE Tumbleweed a package is available from the official repositories. To install it simply run:
zypper install iamb
Snap
A snap for Linux distributions which support the packaging system.
snap install iamb
License
iamb is released under the Apache License, Version 2.0.