Docs: https://b1rd33.github.io/tgctl-go/
A single static tg binary that drives your real Telegram account from the command line. Send messages, edit them, organize folders, run forum topics, manage admin actions, react, mark-read, backfill history into a local SQLite cache, listen for live updates — all scriptable, all auditable, all behind a JSON envelope.
Go port of the Python tgctl with the same CLI contract, the same exit codes, the same safety gates. One binary, no runtime, no Python required.
Anything you'd otherwise click through Telegram Desktop to do, but at scale or on a schedule. Concrete uses people are running it for today:
- Notifications and ops — send build failures, deploy completions, oncall pings to a chat from CI:
tg send -100123 "deploy ok" --allow-write - Customer-support triage — backfill a support chat, search, sort by intent, auto-reply with
--idempotency-keyso retries are safe - Personal automation — cron-driven reminders, scrape link-bot output, archive media to disk
- Migrations and audits — adopt your existing Telethon session via
tg import-telethon-session, then export message history into the local SQLite cache for offline analysis - Building bots without Bot API — full MTProto user-account access via
gotd/td, not the limited Bot API
It is not meant to spam, scrape contacts, or evade rate limits — there's a sliding-window rate limiter and an audit log specifically to keep you on the safe side of Telegram's terms.
# Homebrew (Mac / Linux):
brew install b1rd33/tap/tgctl-go
# Anyone with Go:
go install github.com/b1rd33/tgctl-go/cmd/tg@latest
# Pre-built binaries (Linux / macOS / Windows × amd64 / arm64):
# https://github.com/b1rd33/tgctl-go/releases/latest-
Register an app at https://my.telegram.org/apps to get an
api_idandapi_hash. -
Drop them in
.env:cp .env.example .env # edit TG_API_ID and TG_API_HASH -
Authorize the account:
tg login
You'll get an SMS code in your Telegram app; paste it back. 2FA password is supported.
-
Populate the local entity cache so chat-id-keyed commands work:
tg backfill-entities
Coming from the Python tgctl? Skip steps 3–4 and reuse your existing session:
tg import-telethon-session ~/path/to/python/tgctl/accounts/default/tg.sessionUse an isolated account while testing agent flows:
tg accounts-add test
tg --account test login
tg --account test backfill-entities
tg --account test discover --allow-write
tg --account test send 1240314255 "hello from test account" --allow-write --jsonRun login from the directory containing .env, or export
TG_API_ID / TG_API_HASH. See the
Quickstart and
Library use docs for the
agent subprocess pattern.
tg me --json | jq -r '.data.user_id' # -> 1240314255
tg send 1240314255 "hello from tgctl-go" --allow-writetg send-by-username @durov "👋" --allow-writeID=$(tg send 1240314255 "draft" --allow-write --json | jq -r '.data.message_id')
tg edit-msg 1240314255 $ID "final wording" --allow-write
tg react 1240314255 $ID "👍" --allow-write
tg pin-msg 1240314255 $ID --allow-write
tg delete-msg 1240314255 $ID --confirm 1240314255 --allow-writetg send -1001234567890 "deploy $(git rev-parse --short HEAD) ok" \
--allow-write \
--idempotency-key "deploy-$(git rev-parse HEAD)"A second run with the same key returns idempotent_replay: true instead of double-sending.
tg show 1240314255 --limit 20
tg search 1240314255 "invoice" --limit 50
tg list-msgs 1240314255 --since 2026-04-01 --until 2026-05-09
tg get-msg 1240314255 30350tg folders-list
tg folder-create "support" --include-chats 1001,1002,1003 --emoji 🛟 --allow-write
tg topic-create -1001234567890 "Q3 Releases" --allow-writetg chat-title -1001234567890 "Renamed Group" --allow-write
tg promote -1001234567890 1240314255 --allow-write --confirm 1240314255
tg ban-from-chat -1001234567890 5555555555 --allow-write --confirm 5555555555
tg chat-members -1001234567890 --limit 100 --jsontg listen --json
# → emits {"command":"listen.event","data":{"update_kind":"new_message",...}}
# per incoming Telegram update. --once for one-shot tests.tg accounts-add work
tg accounts-use work
tg login # logs in as the work account
tg --account personal me # one-off overrideEvery Telegram-side write goes through a fixed pipeline you can trust:
write gate → idempotency lookup → fuzzy gate → resolve → dry-run →
local rate limit → audit_pre → Telegram call → idempotency record
| Flag / env | Effect |
|---|---|
--allow-write or TG_ALLOW_WRITE=1 |
Required for any Telegram-side write |
--read-only or TG_READONLY=1 |
Rejects all writes, even with --allow-write |
--fuzzy |
Allows title-like selectors (e.g. "Bjørn") on write commands |
--confirm <resolved-id> |
Required for destructive ops (delete-msg, leave-chat, ban, promote, terminate-session) |
--idempotency-key <key> |
Replays the prior envelope instead of re-sending |
--dry-run |
Returns the resolved payload without contacting Telegram |
| Audit log | NDJSON at accounts/<name>/audit.log; one entry pre-call, one post-call, sharing a request_id |
Stable exit codes (0–9): OK, GENERIC, BAD_ARGS, NOT_AUTHED, NOT_FOUND, FLOOD_WAIT, WRITE_DISALLOWED, NEEDS_CONFIRM, LOCAL_RATE_LIMIT, PREMIUM_REQUIRED.
Every command emits one of:
{"ok": true, "command": "send", "request_id": "req-abc12345",
"data": {"message_id": 30350, ...}, "warnings": []}
{"ok": false, "command": "send", "request_id": "req-xyz09876",
"error": {"code": "FLOOD_WAIT", "message": "...", "retry_after_seconds": 30}}Pipe through jq and script with confidence — the shape is locked.
MIT. See LICENSE.
See CHANGELOG.md and the conventional-commits git history for the implementation arc.