tgctl-go
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.
What it's for
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-key so 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.
Install
# 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
Setup (one time)
-
Register an app at https://my.telegram.org/apps to get an api_id and api_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.session
Agent quick setup
Use 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 --json
Run 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.
Examples
Send a message to yourself (Saved Messages)
tg me --json | jq -r '.data.user_id' # -> 1240314255
tg send 1240314255 "hello from tgctl-go" --allow-write
Send by username (no cache needed)
tg send-by-username @durov "👋" --allow-write
Edit, react, pin, delete
ID=$(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-write
Idempotent retries from cron / CI
tg 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.
Read locally, no network
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 30350
Folders and topics
tg folders-list
tg folder-create "support" --include-chats 1001,1002,1003 --emoji 🛟 --allow-write
tg topic-create -1001234567890 "Q3 Releases" --allow-write
Admin
tg 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 --json
Live event stream
tg listen --json
# → emits {"command":"listen.event","data":{"update_kind":"new_message",...}}
# per incoming Telegram update. --once for one-shot tests.
Multi-account
tg accounts-add work
tg accounts-use work
tg login # logs in as the work account
tg --account personal me # one-off override
Safety contract
Every 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.
JSON envelope
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.
License
MIT. See LICENSE.
Contributing
See CHANGELOG.md and the conventional-commits git history for the implementation arc.