✈️ telecrawl
September 23, 2026 · View on GitHub

Telegram archive CLI.
telecrawl reads local Telegram Desktop tdata archives and native Telegram
for macOS Postbox databases, stores a searchable SQLite archive in
~/.telecrawl/telecrawl.db, and can back it up to GitHub as encrypted age
shards.
It is local-first:
- Normal archive/search commands do not upload data.
backup pushuploads only age-encrypted shards when you run it explicitly.- Telegram message text, chat names, sender names, contact phone numbers, contact usernames, avatar path metadata, and media metadata stay inside encrypted backup payloads.
Install
brew tap openclaw/tap
brew install telecrawl
Official Homebrew macOS binaries require macOS 13 or newer as of Telecrawl 0.4.0. They are signed with the OpenClaw Foundation Developer ID and notarized by Apple before publication. Source builds and cross-platform snapshots remain credential-free.
Or install with Go:
go install github.com/openclaw/telecrawl/cmd/telecrawl@latest
Source builds require Go 1.27.0 or newer (macOS 13 or newer on macOS). CI checks Go 1.27.0 separately; preferred builds and Docker use Go 1.27.1. This source-build requirement does not change the support claims of older released binaries.
Maintainer release gates are documented in docs/releasing.md.
Docker
docker build -t telecrawl .
docker run --rm -v "$HOME/.telecrawl:/data" -v "$HOME/Library/Application Support/Telegram Desktop/tdata:/tdata:ro" telecrawl --source /tdata doctor
docker run --rm -v "$HOME/.telecrawl:/data" -v "$HOME/Library/Application Support/Telegram Desktop/tdata:/tdata:ro" telecrawl --source /tdata import
Mount Telegram Desktop tdata read-only and keep the archive/config under /data.
Setup
No language runtime setup is required. telecrawl imports Telegram Desktop
tdata and native macOS Postbox data through the Go binary.
Import
telecrawl doctor
telecrawl import
telecrawl status
Import defaults to:
- latest
200dialogs - latest
500messages per dialog
Imports merge into the existing archive by default. Chats and messages outside the fetched window remain stored.
Use 0 for no limit:
telecrawl import --dialogs-limit 0 --messages-limit 0
For a destructive full restore, explicitly replace the archive with the import:
telecrawl import --dialogs-limit 0 --messages-limit 0 --restore
--restore deletes all existing archive rows before storing the fetched import.
It cannot be combined with --chat. Default merges require the same Telegram
account identity as the existing archive, even when the source path is unchanged;
use --restore when intentionally switching the archive to a different source.
On the first import after upgrading an archive with legacy source metadata, use
--adopt-source once to assert that the current Telegram account belongs to
this archive without deleting rows. Message overlap is not treated as account
proof because different accounts can share the same group or channel history.
--adopt-source cannot override a different already-canonical source and cannot
be combined with --restore.
Large native Postbox databases may contain an all-zero SQLite reserved locking page at the 1 GiB boundary. Imports preserve that reserved page and continue to authenticate every data page; other zero pages or invalid HMACs abort the import. Native decryption still loads the database into memory, so large sources require substantial available RAM.
Maintainers can reproduce the locking-page import with a generated synthetic database (more than 1 GiB of disk and several GiB of RAM):
make build
TELECRAWL_LOCKING_PROOF_BINARY="$PWD/bin/telecrawl" go test -count=1 -timeout=10m -v ./internal/telegramdesktop -run '^TestSQLCipherLockingPageCLI$'
Native Postbox imports require a decodable authorized account peer ID. A shared
lane encryption key is not account identity; missing or malformed account state
now stops the import before archive writes. Existing archives bound by older
versions to a key-derived identity are retained unchanged and cannot be
automatically attributed or rebound. Keep those archives and their media; use a
separate archive for verified imports until you have chosen an explicit operator
migration or reimport procedure. --adopt-source does not override that binding.
Canonical chats, folders and memberships, topics, contacts, groups and participants, and messages retain explicit Telegram tombstones with deletion time, source, and reason. Missing rows in a bounded import are not deletions. Message identities remain stable across imports, and observable Telegram edits and explicit deletes are retained as append-only revision events. Upgrading an older archive seeds a baseline observation for every existing message before a later edit can replace its canonical payload.
Add --fetch-media when you also want Telegram cloud media that is not cached
locally:
telecrawl import --dialogs-limit 0 --messages-limit 0 --fetch-media
Remote media fetches are bounded best-effort operations. Import stats report how many remote media candidates were attempted, downloaded, still missing, unavailable, timed out, or errored.
Repeat imports reuse existing archived media for the same source before remote
fetch is attempted, so --fetch-media only tries media that is not already in
the local archive.
Native Postbox can tag link previews, polls, geo/live-geo, service messages, or
deleted messages as broad media candidates. telecrawl archives their decoded
message metadata separately from binary media, and only keeps them as media rows
when Telegram returns a downloadable file.
metadata_json is a local source-native Postbox payload for later rendering or
search; it is not a cross-source schema and can contain private Telegram
metadata.
When no --source is provided on macOS, telecrawl checks Telegram Desktop
tdata first, then the native Telegram for macOS group container. No backend
flag is needed. To import a copied archive directly:
telecrawl import --path "$HOME/Library/Group Containers/6N38VWS5BX.ru.keepcoder.Telegram"
Native macOS imports include every local account-* database they find; if more
than one account is present, stored chat and sender IDs are account-scoped to
avoid collisions. They archive cached media by default and store Telegram peer
records as contacts for message enrichment. Contacts can include phone numbers,
usernames, and archived avatar paths when those values exist locally, and are
visible through telecrawl contacts. --fetch-media also uses the existing
native Telegram session to fetch missing cloud media when account auth data is
present; this does not launch Telegram or start a login/2FA flow.
Useful reads:
telecrawl folders
telecrawl contacts
telecrawl chats --limit 20
telecrawl chats --folder FOLDER_ID
telecrawl chats --folder FOLDER_ID --unread --limit 20
telecrawl chats --unread
telecrawl topics --chat CHAT_ID
telecrawl messages --limit 20
telecrawl messages --chat CHAT_ID --after 2026-01-01
telecrawl messages --chat CHAT_ID --topic TOPIC_ID
telecrawl messages --chat CHAT_ID --pinned
telecrawl search "query"
telecrawl search "query" --chat CHAT_ID --topic TOPIC_ID
Search filters can follow the query as shown above, or precede it. For a query
beginning with a hyphen, put the filters first and use -- before the query:
telecrawl search --chat CHAT_ID -- "-query". Folder and unread filters combine;
the limit applies to matching unread chats within that folder.
Archive, import, and backup commands reject unexpected positional arguments
before opening the archive or changing backup state. Use named flags for paths
and filters; only search accepts a positional query.
Telegram folders, forum topics, reply/thread IDs, pinned messages, edits, forwards, reactions, view/reply counts, and richer media titles are archived when the local source or Telegram API exposes them for the active account. Telegram Desktop forum pagination uses the last-message date (or creation date when Telegram explicitly selects that order). Incomplete, stalled, or failed topic fetches abort the import before archive rows are merged or replaced; reaching the 1,000-page safety bound also returns an error.
Folder rows include explicit membership from Telegram dialog filters; dynamic folder rules are recorded as metadata and may not expand to every matching chat.
Add --json before the command for machine-readable output:
telecrawl --json status
telecrawl --json search "invoice"
Data Paths
Defaults:
- Telegram Desktop source:
~/Library/Application Support/Telegram Desktop/tdata - native macOS Postbox source:
~/Library/Group Containers/6N38VWS5BX.ru.keepcoder.Telegram - archive DB:
~/.telecrawl/telecrawl.db - archived media copied from local Telegram caches, plus Telegram cloud media
when
--fetch-mediais used:~/.telecrawl/media/ - backup config:
~/.telecrawl/backup.json - age identity:
~/.telecrawl/age.key - backup checkout:
~/Projects/backup-telecrawl
Override the archive DB:
telecrawl --db /tmp/telecrawl.db status
Override the Telegram source:
telecrawl --source "/path/to/tdata" doctor
telecrawl --source "/path/to/tdata" import
telecrawl --source "/path/to/6N38VWS5BX.ru.keepcoder.Telegram" import
Backup
Create https://github.com/steipete/backup-telecrawl first, then initialize:
telecrawl backup init
telecrawl backup push
The default backup config points at:
{
"repo": "~/Projects/backup-telecrawl",
"remote": "https://github.com/steipete/backup-telecrawl.git",
"identity": "~/.telecrawl/age.key"
}
Use a different repository or config path:
telecrawl backup init \
--config ~/.telecrawl/backup.json \
--repo ~/Projects/backup-telecrawl \
--remote https://github.com/steipete/backup-telecrawl.git
Inspect backup metadata:
telecrawl backup status
telecrawl backup snapshots
Merge the latest backup into the current archive DB:
telecrawl backup pull
telecrawl status
Default pulls preserve destination-only rows and tombstones. Current backups carry the Telegram account identity inside an encrypted metadata shard, and a merge is rejected when that identity differs from the destination. Legacy backups without this identity can merge only into an empty archive; use explicit restore mode when the local archive must exactly match such a snapshot:
telecrawl backup pull --restore
Every changed backup is a Git commit. Add a non-moving, visible checkpoint tag when needed, then restore that tag, commit, or branch without switching the backup checkout:
telecrawl backup push --tag snapshot/before-migration
telecrawl --db /tmp/telecrawl-history.db backup pull --restore --ref snapshot/before-migration
backup snapshots --limit N lists recent manifest-changing commits and tags.
Keep tag names non-sensitive because Git metadata is not encrypted.
Restore into a throwaway DB for validation:
telecrawl --db /tmp/telecrawl-restore-test.db backup pull --restore
telecrawl --db /tmp/telecrawl-restore-test.db status
Backup Security Model
Backup shards are JSONL, gzip-compressed with deterministic gzip metadata, and encrypted with age before Git sees them.
Git can still see cleartext metadata:
- export time
- public age recipients
- table names
- row counts
- shard paths
- encrypted byte sizes
- plaintext shard hashes
- backup cadence and which encrypted shards changed
Git cannot read message text, chat names, sender names, contact phone numbers,
contact usernames, avatar path metadata, or media metadata without an age
identity. Binary media files and cached avatar files archived in
~/.telecrawl/media/ are local only and are not included in backup shards.
Keep ~/.telecrawl/age.key private. If you lose it and no other recipient can
decrypt the backup, the encrypted backup cannot be restored.
Multi-Machine Backups
On another machine:
telecrawl backup init --no-push
cat ~/.telecrawl/backup.json
Copy that machine's public recipient into the first machine's
~/.telecrawl/backup.json, then re-encrypt current shards:
telecrawl backup push
The private AGE-SECRET-KEY-... identity must not be committed or shared.
Reset
Remove local state:
rm -rf ~/.telecrawl
Remove only the archive:
rm -f ~/.telecrawl/telecrawl.db ~/.telecrawl/telecrawl.db-*
Do not delete ~/.telecrawl/age.key unless you have another working backup
recipient or you no longer need to restore existing encrypted backups.