Using tg
From the first login to sending, in the order you will need it. Every command and option is in commands.md; this page explains how they fit together.
Each command does one thing, prints its answer and exits. Only tg watch, tg serve and tg mcp
stay running, and each of them says so.
tg [profile] [options] <resource> <action> [arguments]The first minute
npm install -g @leemour/tg-cli
tg session start # the app from my.telegram.org, then a QR code to scan
tg chats list --limit 5 # your newest chats
tg messages list me # Saved Messages, the latest 20Nothing more is needed to read.
Log in
tg session start # QR code: Settings → Devices → Link Desktop Device
tg session start phone # phone number, the code Telegram sends, your 2FA password
tg session start --qr-file login.png # the QR code as a picture, for an agent to show youThe first login also asks for your own Telegram app from my.telegram.org; --app auto fills in the
site for you. Both steps, and what is kept where: sessions.md.
No secret is ever an argument. The app hash and the 2FA password are asked without showing what
you type; the login code and the phone number are asked, or read from stdin. An argument is visible
to every process on the machine in ps, and stays in your shell history.
For CI, TG_API_ID and TG_API_HASH give the app without the keyring; they win over it.
tg account show # who this profile is logged in as; the phone as its last four digits
tg account show --show-phone # the whole phone number
tg account sessions list # every device and app logged in to the account; ends nothing
tg session end # log out on Telegram's side, and delete the session heresession end ends the session on Telegram's side too: the device disappears from the app's list.
To end a session from elsewhere, use the app: Settings → Devices.
Profiles: the first word
Several accounts live side by side. The profile is the first word, not an option:
tg chats list # profile "default"
tg work chats list # profile "work"
export TG_PROFILE=work # or for a whole shell sessionThe first word is the profile whenever it is not a command. So a profile cannot be called chats:
such a name is refused, with the reason. A name is letters, digits, dot, dash and underscore.
Each profile has its own session, its own app, its own settings and its own list of allowed
recipients. TG_PROFILE_LOCK pins a process to one profile, so an agent cannot pick one with fewer
limits (sessions.md).
Naming a chat
Wherever a command takes <chat>, it accepts:
- a title, or part of one:
"Book club",book - an id:
-1001234567890 - a username:
@example_channel mefor Saved Messages
A title that fits more than one chat is an error that lists the candidates with their ids. tg
never guesses: a message sent to the wrong conversation cannot be taken back. Take the id and repeat
the command with it. An id never changes, so once you have it, use it.
messages show and messages context also take a msg: locator in place of the chat and the id,
as messages search --json prints it for each hit.
A person (<person>, in contacts show) is an id, an @username or part of their name.
Reading
Reading marks nothing read. No command below shows the other side that you looked. Only
tg chats mark-read and tg messages list --mark-read do (below).
Chats
tg chats list # newest first, archived chats included
tg chats list --unread --kind group # only groups with unread messages
tg chats list --search book # titles containing "book"; at least 3 characters
tg chats show "Book club" # kind, unread count, last message, who is in it--kind is one of dialog (one-to-one), group, channel or saved. The filters look at the
newest 200 chats. Groups and channels have their own section.
Messages
tg messages list "Book club" # the latest 20, oldest first
tg messages list "Book club" --limit 50
tg messages show "Book club" 4242 # one message
tg messages context "Book club" 4242 # it, and 5 messages either side
tg messages context "Book club" 4242 --before-n 2 --after-n 10In context, the message you asked for is marked ◀ in the terminal and "anchor": true in JSON.
What needs an answer
tg inbox # other people's unread messages, in every chat
tg inbox --since-time 2h # everything that came in during the last two hours
tg inbox --new # what arrived since the last --new — for scheduled runs
tg inbox --new --jsonl # the same for a script: one message per lineinbox shows other people's messages, never yours, each with its chat. It leaves out muted and
archived chats unless a message mentions you or replies to you; --all takes them in. It says on
stderr how many it left out.
inbox --new moves a saved point. The next --new starts from where this one stopped, so each
message is shown once. The first --new looks back 24 hours. inbox without --new, and
inbox --since-time, leave the point where it is. Plain inbox answers the same until the messages are
read in the app, since it marks nothing read.
One run reads at most 20 chats; the rest are named on stderr and in skipped, with the command that
reads one. In a chat with more than --limit messages waiting, the newest are shown, and stderr says
how to read the rest.
Who owes what: review
tg review # the last 3 days
tg review --since-time 2026-09-23T09:00 # from where the last review ended
tg review --chat "Book club" --jsonEvery message — yours and other people's — in every chat where something happened since --since-time.
It is for working out what you promised, what you are waiting for and what is still unclear; sorting
it is your job or an agent's. It marks nothing read.
The command ends with a line on stderr: from when to when it read. Start the next review from
that --since-time, and nothing falls between two reviews. When the review is incomplete — too many
chats at once, or a chat cut short to its newest 300 messages — it says so, and it is better not to
move the boundary. It reads at most 20 chats in one run.
Unanswered questions
tg review --unanswered # questions nobody answered in 24 hours
tg review --chat "Neighbours" --unanswered 4h--unanswered [hours] keeps only questions waiting for you or for a group's admins. A question is a
message with ? in it (a link's ? does not count), or a reply to you or to an admin. It is
answered when you or an admin replied to it, or were the next to speak after the person who asked.
Questions younger than the hours given (24 by default) are left out: nobody has had time to answer.
When a group's admins are not known, the command says so, and only your answers count.
Voice messages
tg messages transcribe "Book club" 4242 # by Telegram where it can, else a model here
tg messages transcribe "Book club" 4242 --local # only the model on this machine
tg messages list "Book club" --transcribe # every voice message shown that has no text yet
tg inbox --transcribe
tg review --transcribe
tg messages list "Book club" --transcribe --model gigaam-v3Telegram transcribes for Premium accounts, and a few messages a week on the free trial. Without it, a model on this machine does the work, and the recording never leaves the computer. A model is downloaded once, and only when you ask:
tg models audio list # the models, which is downloaded, which is the default
tg models audio download parakeet-v3 # once, checked against the sha256 this version expects| Model | Languages | Size |
|---|---|---|
parakeet-v3 — the default | 25: Bulgarian, Czech, Danish, German, Greek, English, Spanish, Estonian, Finnish, French, Croatian, Hungarian, Italian, Lithuanian, Latvian, Maltese, Dutch, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian, Swedish, Ukrainian | 670 MB |
gigaam-v3 | Russian — the best of the three for Russian | 232 MB |
gigaam-v3-ctc | Russian — a little faster, rougher with capital letters | 225 MB |
--model picks another model for one command, beside --transcribe or in messages transcribe; transcribeWith and speechModel in the settings
choose the defaults (configuration.md). A transcript is kept in the local store,
so asking again answers at once. --transcribe can take minutes.
Files
tg messages download "Book club" 4242 --output-dir ~/Downloads # one message's files
tg messages download "Book club" --all --output-dir ~/tg-files # every file of the chat, newest firstPhotos, files, videos and voice notes are saved; the folder is created if it is missing. A file
keeps its own name; one without a name gets the message id. A download never overwrites a file.
With --all, a name already taken gets the message's id in front. --all remembers where it stopped
in a small file beside the downloads, and the same command continues from there. Saved files are
readable only by you.
People
tg contacts list # people you have a one-to-one chat with, newest first
tg contacts list --order name --search ann
tg contacts show @example_user # their bio and the chats you share
tg contacts lookup # who has a phone number — asks for it, or reads it from stdin
tg contacts sync # your whole Telegram contact list into the local storecontacts list is the people you have a one-to-one chat with. contacts sync brings in the rest of
your Telegram contact list too. contacts lookup never takes the number as an argument: pipe it in,
or type it when asked.
Changing the address book and your profile:
tg contacts add @example_user # under the name they show
tg contacts rename @example_user Ann "from work" # a name only you see
tg contacts remove @example_user # the chat stays
tg contacts block @example_user # they need not be a contact
tg contacts unblock @example_user
tg contacts import people.txt # one "number, name" per line; never numbers as arguments
tg account update --first-name Ann --description "about me" --photo me.jpg
tg account sessions end --others # logs out every other device, your phone too; asks firstcontacts import answers how many it sent and who Telegram knew, never a number. account sessions end asks before it goes; --yes answers in a script.
Pages
A list shows limit rows (20 by default). chats list, contacts list, chats members list and
topics take --page and --all:
tg contacts list --limit 5 # five a page
tg contacts list --limit 5 --page 2 # the sixth to the tenth
tg contacts list --all # every row, no paging⚠ A page number over a live list can repeat or skip a row. The newest is on top, so a message that arrives between page one and page two moves someone across the border.
A chat's messages have no pages: they have --before-id, --after-id and --after-time, which page exactly:
tg messages list "Book club" --before-id 4242 # older than message 4242
tg messages list "Book club" --after-id 4242 # newer than 4242, oldest first
tg messages list "Book club" --after-time 2h # what came in during the last two hours
tg messages list "Book club" --after-time 2026-09-20T09:00
tg messages list "Book club" --before-time 1d # what came before this time yesterdayIn the terminal, the line that names the next page goes to stderr. --before-id and --after-id take
a message id. --after-time takes ISO 8601, or "this long ago": 30m, 2h, 1d. In
messages context, --before-n and --after-n are counts of messages: there the point is already
the message.
Find a chat, then write to it
tg chats list --search book --kind group # groups with "book" in the title
tg contacts list --search ann # people by name or @username
tg messages search "contract" # the text of every message this machine has kept
tg messages search "contract" --chat "Book club"
tg messages search "invoice.*(march|april)" --regexA search needs at least three characters. messages search looks for every word, as a word or
the start of one — invoic finds "invoice". It never connects to Telegram: it answers from what was
read, fetched or kept by serve (archive.md). Once you have the chat, use its id.
Sending
Nothing sends unless you typed a command that sends, and it asks no confirmation: the chat and
the text are already in the line you typed. Every send goes through the send guard: a read-only
profile, the profile's allow list, the list of allowed recipients and the hourly limit
(security.md). Every attempt is logged, never its text:
tg sends list.
tg messages send me "a note to myself"
tg messages send "Book club" "See you at 7" --silent # no notification
tg messages send "Book club" "a link, no card" --no-preview
tg messages send "Book club" "**Bold** and _italic_" --md # bold, italic, struck, code--md reads bold (**), italic (_), struck (two tildes) and code (backticks), nothing else. A mark
counts only at a word's edge, so file_name stays as typed; \ keeps a mark literal. Without it,
the text goes as typed. messages edit takes --md too.
Text from stdin
Leave out the text and it is read from stdin. That is the only way to send several lines, and it
keeps the text out of ps and your shell history:
printf 'first line\n\nthird line' | tg messages send me
tg messages send "Book club" < note.txtSending later
tg messages send "Book club" "Tomorrow" --at-time 2026-10-01T09:00 # local time
tg messages send "Book club" "In two hours" --at-time 2h # or 30m, 1d from now
tg messages scheduled "Book club" # what waits to be sent there--at-time hands the message to Telegram, which sends it even with this machine off. The time is rounded
down to the minute. Less than a minute from now, or more than a year, is refused. The guard counts a
scheduled message in the hour Telegram sends it. Cancel or change one in the Telegram app; tg
does not.
Files, photos and voice messages
tg messages send "Book club" "The agenda" --file agenda.pdf # byte for byte; the text is the caption
tg messages send "Book club" --photo picture.jpg # recompressed by Telegram
tg messages send "Book club" --file trip.mp4 # a video plays in the chat
tg messages send "Book club" --file trip.mp4 --as-file # the same video as a file to download
tg messages send "Book club" --voice note.ogg # a voice message, alone, with no text--photo takes a .jpg, .png or .webp. A .mp4 or .mov given with --file goes as a video
unless you add --as-file. --voice takes an Ogg Opus file (.ogg, .oga, .opus) and goes alone:
no text, no other file. Hidden files and folders, ~/.ssh, tg's own folders
and the local store are refused unless you add --allow-any-file — that is where keys and tokens
live.
Replying
tg messages send "Book club" "Agreed" --reply-to 4242A reply is a send, so every send option works with it.
When the outcome is unknown
Exit code 14 means the connection broke after the message left: it may have gone. The error
carries a --send-id. Repeat with it, and Telegram drops the second copy:
tg messages send "Book club" "See you at 7" --send-id <id from the error>
tg messages forward "Book club" 4242 --to me --send-id <id from the error>A forward and a poll carry one too. A repeat without it is a second message to a person. A message sent with --at-time is never repeated:
look in tg messages scheduled <chat> instead.
Editing, forwarding, pinning, deleting
tg messages edit "Book club" 4242 "the corrected text" # your own message; --md as in a send
tg messages forward "Book club" 4242 --to me # checked against the chat it goes to
tg messages pin "Book club" 4242 # quiet unless --notify
tg messages unpin "Book club" 4242
tg messages delete me 4242 4243 --allow-dangerous # at most 10, for you only
tg messages delete me 4242 --allow-dangerous --for-everyoneAn edit reaches people who may have read the old text already. A forward is a new message: it goes
through the same guard as a send, against the chat it goes to. A deletion cannot be undone, which is
why it asks first: answer y, or add --allow-dangerous to skip the question. In a supergroup or a
channel Telegram deletes only for everyone, so there only --for-everyone works.
What counts toward the hourly limit: a message, a forward, an edit, a pin that notifies, and each deleted message. A reaction and a quiet pin do not.
Reactions and polls
tg reactions add "Book club" 4242 👍 # replaces the reaction you had
tg reactions remove "Book club" 4242
tg polls show "Book club" 4250 # the poll and its answer ids
tg polls vote "Book club" 4250 <answer id>
tg polls vote "Book club" 4250 --retract
tg polls create "Book club" "Which day?" Monday Tuesday --anonymous
tg polls close "Book club" 4250 # your own poll; it cannot be reopenedWhen you read a chat, reactions show under a message — 👍 3 🔥 1 (you: 🔥). A vote in a public poll
shows your name to everyone in the chat. Vote by the ids polls show prints, never by an answer's
position. --multiple lets people pick several answers. People can change their vote only in a poll
made with --revote.
Marking a chat read
tg chats mark-read "Book club" # up to the newest message
tg chats mark-read "Book club" --until 4242 # only up to this one
tg messages list "Book club" --mark-read # read it, and mark it read up to the newest shownThe other side sees that you read it. It goes through the guard as the action read, and does not
count toward the hourly limit.
Folders
tg chats folders list # your folders, in the order the app shows them
tg chats folders create "Trips" --chat "Hiking" --chat @kate
tg chats folders update "Trips" --title "Travel" --add "Climbing" --remove @kate
tg chats folders delete "Travel" # the chats stayA folder is named by its id or its title exactly. Only you see your folders; each change still goes
through the guard, as an account change.
Not in tg yet
Several photos in one message, sending into a forum topic. They are on the roadmap.
Groups and channels
tg chats inspect https://t.me/+AbCdEf # where an invite or public link leads; does not join
tg chats members list "Hiking" --all # everyone, with their role and when last seen
tg chats events "Hiking" # who joined, left, was added or removed — 7 days
tg chats events "Hiking" --type join,leave --since-time 2026-09-01T00:00
tg topics list "Hiking" # a forum group's topics, newest activity first
tg topics search "Hiking" "gear"
tg review --chat "Hiking" --unanswered # questions nobody answeredAll of these only read. events reads the chat's service messages: who did what, and to whom. The
names are join, leave, add, remove, create, title and pin.
These change something, and the people in the chat see it:
tg chats create "Hiking 2027" @olga 12345 # a supergroup; the people added are told
tg chats create "Trail news" --channel # a channel; people join it by its link
tg chats join https://t.me/+AbCdEf # by an invite link, or a public one
tg chats leave "Hiking 2027"
tg chats update "Hiking 2027" --title "Hiking 2028" --description "routes and dates"
tg chats update "Hiking 2027" --all-can-pin off --only-admins-add on
tg chats link show "Hiking 2027" # the invite link, if you may see it
tg chats link reset "Hiking 2027" # a new one; the old one stops working
tg chats members add "Hiking 2027" @kate 67890 # they are told
tg chats members remove "Hiking 2027" @kate # their messages stay
tg chats admins add "Hiking 2027" @kate --can pin,delete
tg chats admins remove "Hiking 2027" @kateA new group is always a supergroup. Someone whose privacy settings stop them being added is named
in the answer under providerMetadata.notAdded; the group is made anyway. A group whose admins
approve who joins answers that the request was sent. Each goes through the guard as a chat change,
and each person added counts toward the hourly limit.
chats update changes the title, the description and the two settings Telegram has, in one go; the
answer is the group as it stands, and chats show shows the same settings. Moderation rules —
chats rules and chats moderate — are in groups.md, with everything else there
is for a group you run.
For scripts and agents
At a terminal tg prints a table; into a pipe, or with --json, it prints one JSON value on
stdout and nothing else — no spinner, no tick, no warning. Notes, warnings and errors go to stderr
in every mode.
tg chats list --json | jq -r '.items[].id'
tg messages list me --jsonl | jq -r .text # one message per line--json: one JSON value. Every list is one object, always the same shape:{ "items": [...], "page": 1, "limit": 20, "hasMore": true }.--alland--offlineanswer with the same object.- A chat's messages have no page number:
{ "items": [...], "limit": 20, "hasMore": true }. --jsonl: one object per line, no wrapper; whether there is more is said on stderr only.- An error is
{ "error": { "code": "...", "message": "..." } }on stderr, and stdout is empty, so a refusal can never be taken for an empty result. - Branch on the exit code, not on the text. The text changes; the code does not.
0worked,2bad input,4not logged in,5the profile may not do this,6not found,7not on the list of allowed recipients,8a limit (the hourly limit, or Telegram's own),9Telegram did not answer in time,14unknown whether a message went. The full table is in commands.md. - Ids are strings. Never turn one into a number.
--quietturns notes off; a failure is still said.-vand-vvadd detail to the table view.--timeout 30sbounds the whole command (500ms,30sor2m).tg commands --jsonis the whole command tree, withmutates: trueon every command that changes something in Telegram.
if ! tg messages send "Book club" "See you at 7" --json > /dev/null; then
case $? in
14) echo "it may have gone — repeat only with the same --send-id" ;;
4) echo "run tg session start" ;;
esac
fiAn agent with a terminal reads the skill file for the traps the help cannot explain:
mkdir -p ~/.claude/skills/tg-cli && tg skill show > ~/.claude/skills/tg-cli/SKILL.md # Claude Code
mkdir -p ~/.agents/skills/tg-cli && tg skill show > ~/.agents/skills/tg-cli/SKILL.md # Codex, Gemini CLIAn agent without one (Claude Desktop, Cursor) connects over MCP: mcp.md.
What a conversation looks like
tg messages list and tg messages search print a transcript at a terminal, not a table:
10:05:12 Anna
Shall we call on Thursday?
10:09:03 Boris
↳ Anna: Shall we call on Thursday?
Thursday works.
📎 photo
edited 10:09:30Times are local, and a line marks each new day. ↳ is what a message answers, ↪ whose message was
forwarded, 📎 an attachment. Control characters in a message or a name are shown as text, never run
by the terminal. -v adds the ids of the message, the sender and the chat; -vv everything known
about the message.
New messages as they arrive
tg watch # new messages, until Ctrl-C or --timeout
tg watch --jsonl # one message per line, as messages list --jsonl
tg watch --jsonl | ./on-message.sh
tg watch --events --jsonl # edits, deletions and reactions too
tg watch --jsonl --timeout 2m # a timeout ends it normally, with exit code 0With --events every line names its event: message, edit, delete or reaction. Without it,
the lines are bare messages. Telegram does not say in which chat a message was deleted in a private
chat or a small group, so such a line has no chat.
watch starts from now. What arrived while nothing was listening is not shown. To keep the local
store current, including what came in while this machine was off, use serve, in the background or
as a system service (archive.md):
tg server start # serve in the background; answers once it is connected
tg server status
tg server install # a systemd user unit or a launchd agent; starts nothingWhat a command did
tg --trace chats list # show each request on stderr, keep nothing
tg --record chats list # keep it, show nothing
tg runs list # what was kept, newest firstA failed run is always kept. A record holds operations, ids, counts and durations — never a message, a name, a chat title, a phone number or a key. In full: diagnostics.md.
The local store
Everything tg reads is kept on this machine, so that it can answer without the network:
tg chats list --offline # only from the store, never connect
tg store fetch "Project Alpha" --estimate # how much a fetch would take
tg store fetch "Project Alpha" --background # a chat's history, as a job
tg store export "Project Alpha" --format markdown --output alpha.md
tg store backup ~/tg-store.db # a copy of the store, while it is in useAn ordinary command still asks Telegram. --offline is for when there is no network, or when
connecting is not wanted; a send with --offline is refused. Fetching, export, search, backup and
the service: archive.md.
Settings, and what a profile may do
Settings live in an optional config.json, and every value is decided in one order: option →
environment variable → the profile in the file → the file's defaults → built in.
tg config show # every setting, and where it came from
tg config set limit 50
tg work config set permissions.messages readonly # profile "work" changes no messages
tg config set permissions.messages.send ask # a yes or no before each send
tg config set sendsPerHour 10permissions says what a profile may do, per command: deny, readonly, ask or allow. By
default everything is allowed, and deleting messages and ending sessions ask first. A refusal is
exit code 5, and the error names the command that allows it. The file has no field for a
secret. Every setting and variable: configuration.md.
Next
- archive.md — the local store: search, fetch a chat's history, export, backup
- configuration.md — settings, and what a profile may do
- security.md — what reaches the disk, and the send guard
- recipes.md — daily work for an agent