Two small command-line tools that run on your own machine: telegram-tools for your own Telegram account, discord-tools for your own Discord servers. This page covers installing each one, the one-time setup, and what every command does. The two commands that cannot be undone get their own section.
Both need Python 3.11 or newer. There is no account to make and no server to point them at. After the setup below you type the tool’s name and pick from a numbered menu; the same jobs are also available as commands, for scripts and for agents.
telegram-tools
Telegram gives every chat, channel and forum topic a number, and then hides it. This finds those numbers, and does the handful of jobs the app makes tedious: searching an old thread, saving one to a file, emptying a forum topic without losing the topic. It signs in as you, the way the desktop app does.
Install
pip install telegram-tools
# or
pipx install telegram-tools
# or
uv tool install telegram-tools
Set it up
Telegram will not let a program sign in as you without your own API key. Getting one takes about two minutes, once.
- Open https://my.telegram.org/apps and log in with your Telegram phone number.
- Fill in the short “Create new application” form. Any name and short name will do; for platform, pick Desktop.
- Copy the App api_id (a number) and the App api_hash (a long string of letters and digits).
- Put them in the file the tool reads:
mkdir -p ~/.telegram-tools
cat > ~/.telegram-tools/.env <<'EOF'
TELEGRAM_API_ID=123456
TELEGRAM_API_HASH=your-api-hash-here
EOF
Treat the api_hash like a password. Shell environment variables and a .env in
the folder you are standing in also work, and both win over
~/.telegram-tools/.env.
Then log in:
telegram-tools auth # your phone number, then the code Telegram sends
telegram-tools auth --qr # or scan a code from a phone that is already signed in
Either way it happens once, exactly like signing in on a new device — the login
is saved and reused afterwards. If your account has two-step verification, the
password is asked for at the prompt and stored nowhere. auth --qr needs one
extra library, pip install 'telegram-tools[qr]', to draw the code; open
Telegram on the signed-in phone, Settings → Devices → Link Desktop Device, and
scan it.
telegram-tools auth --logout ends the session after you type the profile’s
name back. The login also shows under Telegram’s Settings → Devices, like any
other device.
More than one account
Each login is a profile, and you can have several:
telegram-tools auth --profile work # log a second account in
telegram-tools --profile work discover # act as it — the flag goes first
telegram-tools profiles # what this machine is logged in as
Every profile keeps its own login in ~/.telegram-tools/profiles/<name>/,
written so only your user can read it, beside a small record holding a label and
the account id — never your phone number, never a password. TELEGRAM_TOOLS_PROFILE
sets which one is the default; with nothing set it is default.
If you used telegram-tools before profiles existed, nothing moved: your existing
login is the default profile, read from where it has always been. doctor
mentions it could move into a profile folder, and auth --migrate does it after
asking.
Which account am I about to use?
Every command and every menu screen opens with the answer:
Acting as: Sven (@sven) · account · Target: Agency › 💻 Deploys (-1001234567890)
That is there because sending the right message to the wrong account is the mistake worth catching before it happens, not after. A phone number is never part of that line, and the file your login lives in is never printed anywhere.
Posting as one of your bots
telegram-tools --as-bot alerts send --chat -1001234567890 --topic 141 --text "deploy is green"
--as-bot goes before the command, like --profile, and names a bot whose token
you have stored under that nickname in TELEGRAM_BOT_TOKENS. The message then
comes from the bot, and the line at the top says so, naming both the bot and the
account it belongs to:
Acting as: @alertsbot · bot (via Sven (@sven)) · Target: Agency › 💻 Deploys (-1001234567890:141)
A bot can do less than you, not more. Telegram gives a bot no list of chats, no
history and no search, so under --as-bot only send and create topic run;
anything else refuses and tells you to run it without the flag. The bot has to be
a member of the chat already, --chat has to be an id or an @username (a bot
cannot look up a link), and --yes needs the destination in your send allowlist
exactly as it does for you. The bot’s token is never written to disk, and no
screen ever prints it.
Going through a proxy
# in ~/.telegram-tools/.env, or a single profile's own .env
TELEGRAM_PROXY=socks5://127.0.0.1:1080
socks5://, socks4:// and http:// work, with an optional user:password@.
It needs pip install 'telegram-tools[proxy]' — and without that library the
command refuses to run rather than quietly connecting from your own address,
which is the one outcome someone asking for a proxy must not get. doctor tells
you before you run anything.
The commands
Run telegram-tools with nothing after it and the menu opens. Every command
below has a row in it, so you never have to remember a flag.
discover— your chats, channels and forum groups with their real IDs, and every topic ID inside a forum. It shows chats you manage;--allshows everything, and--jsonwrites the list to a file instead of the screen.search— finds messages in one chat by text, sender, date range or topic. Prints a table, or writes JSON, CSV, JSON lines, Markdown or a self-contained HTML page when you give it--output.search --archiveanswers the same flags from the local archive, without connecting.archive— a local, searchable copy of everything your account can read.archive syncfills it — every chat, every topic of every forum group — and resumes where it stopped, so a sync you interrupt never writes a row twice and a second run fetches only what is new; it ends with a table naming every chat it could not read and why.archive search --query "deploy AND green"is full-text search over it, ranked, with the match marked, and it never touches Telegram;--context 2shows the messages around each hit.archive exportwrites one search in any of the five formats (a bare name lands in~/.telegram-tools/exports/),archive statussays what it holds against its 2 GiB budget, andarchive retention/archive forgetprune it — dry-run by default, and for real only after you type the chat’s exact title, likedelete. The archive needs a Python whose SQLite has FTS5;doctorsays.review— the links and files the archive has seen, in one queue, and the only way anything is ever downloaded.review listshows what is waiting and asks nothing of any host;review approvefetches the ones you pick into a quarantine folder after ay/N, runs them through the built-in checks and a local ClamAV if you have one, and gives each a verdict;review acceptshows that verdict and, after a secondy/N, keeps the file. A file the checks refused or the scanner flagged cannot be accepted; with no scanner the verdict saysUNSCANNED, never “clean”. Neither step has a--yes, and both refuse when no person is at the terminal.review rejectdeletes what was fetched,review retrycontinues a download that was cut off, andreview statussays what is held and whether a scanner was found.send— posts a message, a file, or both, to a chat or into one forum topic, or as a reply to a message (--reply-to). Several files go as one album. It shows you the whole message, where it is going and whom it mentions before anything is sent.message— what you do to a message once it exists:reply,edit,delete,forward,copy,react,unreact,pin,unpin,poll,typing,read,unread,bookmarkanddraft. Each shows the chat and the message it is about to act on — id, date, sender, first line — then asks.deletelists every id it would remove and stops; for real it takes--executeand a typedDELETE, is never bigger than--limit(200) unless you raise it, and above 1000 messages needs--i-knowand the count typed too.--from-searchselects from the local archive by query.copyre-posts the text and links to an attachment rather than fetching it;bookmarkforwards to Saved Messages and notes it in the archive.create—create group,create channel, orcreate topicinside a forum group, and prints the new ID.--forummakes a group with topics already switched on.structure— a chat’s shape as a file, so a forum with the right topics and rights is repeatable.structure exportwrites a blueprint: kind (supergroup, forum or channel), title, description, topics with their icons, default rights, slow mode and whether joining needs approval — and prints, every time, what it does not carry: members, admins, messages, history, invite links, the linked discussion group. Those are listed in the file too, generated from the same allowlist the exporter uses, so a field outside it cannot reach a blueprint.structure diffsays what another chat would need to match the file and changes nothing.structure applydry-runs by default, listing every step; for real it takes--executeand the chat’s exact title typed back, then makes the missing topics and settings one step at a time and never deletes anything the chat has beyond the file.--createmakes a new chat of the blueprint’s kind first.structure remapprints the id table an apply wrote, without connecting. A basic group has no blueprint, for the reasondeleterefuses one:createmakes supergroups.admin,member,join-requests,invite,settings— running a group or channel.admin listshows the creator and every admin with their rights;admin promote,admin rightsandadmin demotechange who holds what.member listshows who is in (or, with--banned, who is out);member ban,unban,mute,unmuteandrestrictact on one person, and a mute or restriction needs--until, a minute to a year.join-requests list,approveanddeclinehandle a chat that needs approval to join.invite listandinvite createshow invite links in full — they are the only two places that do — andinvite revoketakes one back.settings showreads a chat’s title, description, topics flag, slow mode, join approval and default member rights, or — with--topic— one topic’s own title, icon, closed and hidden;settings setchanges any of them, needingchange_infoon a chat andmanage_topicson a topic, and reports the fields that actually moved rather than that the call went through. Every write names the right it needs before it starts and refuses, naming both rights sets, when your account holds the right but cannot use it on that person — an admin gives only rights it holds, and edits only an admin with no more than it has. Banning and demoting ask for the person’s exact label at a terminal, likedelete; a ban’s--reasonis kept in the local audit line only, because Telegram stores none.settings set --forum offasks for the chat’s exact title the same way, because switching topics off makes every topic in the group stop existing.folders— your own chat folders, the shelves above your chat list.folders listshows each with its chats and categories,folders createandfolders editbuild one out of named chats (--include, repeatable) and whole categories Telegram matches for you (--types groups,bots,…), minus what you leave out (--exclude), andfolders deleteremoves the shelf and none of the chats on it, after you type its exact title. Both chat lists replace rather than append andnoneempties one; a field no flag names is left alone, including the chats you pinned inside the folder. A folder that came from a chatlist invite is listed assharedand refuses to be edited or deleted here — Telegram gives it no exclude list and no categories, so what would be written back is not what is there. Folders belong to an account, so--as-botrefuses them.watch— rules over what happens in your chats, and the one process that fires them.watch rules addwrites a rule as a JSON file in~/.telegram-tools/rules/, which stays yours to edit by hand: what to trigger on (message,edit,reaction, a join, a leave, alink, amedia), what to narrow it to (a chat or topic, a sender, a domain, a keyword, a regex, a file type or size), and what to do — alert a chat, alert a command on your PATH, tag, bookmark, record what the platform sent, sync that scope into the archive, or put its links and files in the review queue. That list is the whole of it: a rule can never download anything and never change anything, and anything else is refused when the rule is written.watch runkeeps one process up that receives updates, replays what it missed while it was down and fires the rules; it runs in the foreground, installs no service, and does not take your login’s session file, so your other terminal keeps working.watch status,watch stopandwatch reloadoperate it, andwatch rules test --event FILEsays what a recorded event would do while firing none of it. An alert to a chat goes out through the same allowlist an unattendedsend --yesanswers to.scheduleandsend --at— a message posted later, and the tool always says which of two things you actually have.send --chat C --text "..." --at 2026-09-09T09:00hands the message to Telegram, which posts it with your machine off: server-held.schedule post --chat C --text "..." --every "0 9 * * mon"stores a row your own runner posts, and every listing spells it out:runner-held: fires only while watch run is up on this machine. Telegram has no repeat of its own, so a repeating message is always the second kind.--everytakes an interval (15m,2h,1d) or a cron expression; a time with no offset is your machine’s local time, echoed back with the offset so there is no doubt.schedule list --chat Cshows both kinds together with their guarantee, andschedule cancelcancels either. Scheduling is your account’s, not a bot’s: Telegram gives a bot no way to hand it a message for later.clear-messages— empties one or more forum topics without deleting the topics or changing their IDs. Read before it sends or deletes anything first.bots— lists the bots you own with their IDs, and edits what @BotFather edits: display name, bio, the description people read before pressing Start, commands, profile photo, default admin rights. Three of those edits have to be sent by the bot itself, so they need that bot’s token inTELEGRAM_BOT_TOKENS; everything else runs on your normal login.auth— logs a profile in or out. It asks at the terminal and has no unattended form, which is deliberate: a login is the one thing here that turns a terminal into your account.profiles— the accounts this machine is logged in as, by label. It opens no connection and prints no paths.--as-bot NICK— before any of the above: runsend(but notsend --at),create topic, a message verb, an administration command (where the bot is an admin of the chat) orwatchas one of your own bots instead of as you. Notfolders: a bot has no chat list to shelve. See posting as one of your bots.doctor— checks the setup and tells you what is missing, printing no secrets: which profiles exist, whether the current one is logged in, whether a proxy is usable, whether a virus scanner is on your PATH for the review queue, how much the quarantine folder holds, whether a runner is up and whether your rules load, and whether the tool’s own files are private enough to write to. If they are not, every command that writes refuses until you fix them, anddoctorgives you the exactchmod.
Every command also has a machine-readable form for scripts and AI agents: put
--json before the command name and it prints one object instead of a screen —
the result, what it acted on, and a stable error code with a hint when it
refuses. --jsonl streams a line per row for long answers. Anything a person
would read moves out of the way so the object is the only thing on stdout, and
without the flag nothing changes at all. The object also names the account the
run acted as, so a script driving two profiles can check it used the right one.
auth is the exception a script should never touch: it asks a human for a code.
discord-tools
Discord does not allow a person’s account to be automated, so this one drives a bot instead. You make the bot once — the tool walks you through Discord’s developer site — and after that it does the same set of jobs: IDs, members, history, sending, creating channels, roles and who can do what where, moderating members and invites, reading the server’s audit log, clearing them out. It can also stay running and watch a server, and put something in the calendar or in the queue to post later.
Install
pip install discord-tools-cli
# or
pipx install discord-tools-cli
# or
uv tool install discord-tools-cli
The package is called discord-tools-cli because the plain name was already
taken on PyPI by an unrelated, abandoned package. The command you type
afterwards is discord-tools.
Set it up
discord-tools auth
That one command is the setup. It walks you through the Discord Developer
Portal, checks the token you paste back, checks that the message-content intent
is switched on, saves the token, and prints the invite URL you use to add the
bot to a server. Run discord-tools doctor afterwards to confirm the token, the
intent, the servers the bot joined and its permissions.
The token is stored in ~/.discord-tools/.env, in a file only you can read, as
a named profile:
DISCORD_BOT_TOKENS=default:token-a,dobby:token-b
Run auth once per bot to add another. --profile dobby, written before the
command, picks one for a single run; DISCORD_TOOLS_PROFILE sets your default,
and the menu can switch between them without restarting.
If message text comes back empty on every message, the message-content intent is
off in the portal. doctor names it and auth walks you through turning it on.
A bot per agent
discord-tools auth --profile dobby # set a second bot up
discord-tools --profile dobby send ... # act as it — the flag goes first
discord-tools profiles # what this machine has
discord-tools profiles remove --name dobby
The tokens stay on that one DISCORD_BOT_TOKENS line. What sits beside them is
~/.discord-tools/profiles/<name>/profile.json, holding the bot’s name and the
bot id auth saw when it checked the token — no token, nothing secret. It is
there so a token pasted into the wrong profile is caught: a Discord bot token
carries its own bot id, and if that disagrees with the recorded one the command
stops before making a single call, rather than quietly acting as the wrong bot.
Removing a profile asks you to type its name, the same way deleting a channel does, and says first that the token is not recoverable from here — setting that bot up again means resetting its token in the portal.
Which bot am I about to use?
Every command and every menu screen opens with the answer:
Acting as: harrybot (profile harry) · bot · Target: Agency › 🚨alerts (1394827364512)
That is there because posting the right message as the wrong bot is the mistake
worth catching before it happens, not after. A token is never part of that line.
A token handed over in DISCORD_TOKEN has no profile to name, so the line says
(token from environment) instead — a bot nobody named is the one worth naming.
Going through a proxy
# in ~/.discord-tools/.env
DISCORD_PROXY=http://127.0.0.1:3128
http:// and socks5:// work, with an optional user:password@. doctor
prints the host it will go through and never the credentials.
The commands
Run discord-tools with nothing after it and the menu opens. Every command
below has a row in it.
auth— the guided setup above: portal walkthrough, token check, invite URL.doctor— checks Python, the config, the token, which bot the profile was set up as, the proxy, the file modes, the archive, the virus scanner it would run, the quarantine, the message-content intent and the servers the bot is in.--channeladds the permission checks for one channel or thread, and a test of what the bot can actually see there.profiles— every bot this machine has, by name and by the labelauthrecorded.profiles remove --name <name>takes one off, after you type its name back. Neither needs a working token, which matters when the reason you are looking is that one stopped working.discover— the whole server → channel → thread tree with every ID.--servernarrows it to one server,--jsonwrites it to a file.members— a server’s members with their IDs, usernames and display names, and whether each one is a bot.--outputexports JSON or CSV. This one needs the Server Members intent switched on in the portal as well.search— searches one channel or thread’s history. Discord gives bots no search API, so the tool fetches the history and filters it here, on--keyword,--from-user,--since,--untiland--limit. The printed table shortens long messages; an export carries them whole, in JSON, CSV, JSON Lines, Markdown or HTML.--archiveasks the local archive instead of fetching.archive— the local archive, so a history question is answered once from Discord and every time after from disk.archive syncfetches new history from every channel and thread the bot can read into~/.discord-tools/archive.sqlite, resumes where it stopped if interrupted, and ends with a coverage table saying what it could not read and why: a channel the bot has no history permission in, or the message-content intent being off.archive search --query "…"is full-text search ranked by relevance, with a regex, a sender, dates and the messages around each hit;archive exportwrites the same result in five formats;archive statusshows what is held.archive retentionandarchive forgetprune it, and behave likedelete: a dry-run first, then--executeand the scope’s exact name typed back. Onlysynclogs in; the rest read the file.review— the review queue. The tool never downloads anything on its own: every attachment and every linkarchive syncsees waits here, andreview listshows them — the URL exactly as the message wrote it — without contacting any host, so nobody’s address reaches a link before a person has said yes.review approveasksy/N(there is no--yes; with no terminal it stops withAPPROVAL_REQUIRED) and fetches into quarantine, resuming a download that died partway and refreshing an attachment URL Discord has let expire by re-reading its message. Every fetch runs the same ordered checks: scheme, redirects walked only after approval, private and cloud-metadata addresses refused, path, size, time, archive bombs inspected without extraction, type against extension against magic bytes, checksum, duplicates, and ClamAV if it is installed — the first failure isBLOCKEDwith the check named, and no scanner isUNSCANNED, never a silent pass.review statusshows what a fetch found;review acceptprints the verdict and asks before moving a file into~/.discord-tools/media/, refusingBLOCKEDandINFECTED;review rejectdeletes the bytes;review retryresumes a failed fetch. Onlyapproveandretrylog in.send— posts as the bot, after showing you the whole message first, or as a reply to a message (--reply-to). It pings nobody unless--mentionsays who, and the preview says which.message— what you do to a message once it exists:reply,edit(the bot’s own messages only, which is Discord’s rule),delete,forward,copy,react,unreact,pin,unpin,poll,typingandbookmark. Each shows the channel and the message it is about to act on — id, date, sender, the text — then asks.deletelists every message it would remove and stops; for real it takes--executeand a typedDELETE, is never bigger than--limit(200) unless you raise it, and above 1000 messages needs--i-knowand the count typed too.--from-searchselects from the local archive by query.forwardis Discord’s own forward;copyre-posts the text with a line saying who wrote it where, and links to an attachment rather than fetching it.bookmarkis a local note in the archive file, because a bot has no bookmarks on Discord;read,unreadanddraftsay so too, and exit with the reason.create—create channel,create category, orcreate thread, each behind a confirmation.structure— a server’s shape as a file, so a layout you got right once is repeatable.structure exportwrites a blueprint: the server’s settings, roles, categories and channels with their permission overwrites, forum tags and AutoMod rules, every ID replaced by a name-based handle so the same layout exported from two servers is the same file. It prints, every time, what a blueprint never carries — members, messages, authors, audit history, secrets, integrations, webhooks, invites, bans, emoji, stickers, managed roles — and names what it had to leave out as manual steps.structure diffsays what another server would need to match the file and changes nothing.structure applydry-runs by default, listing every role, category and channel it would make and every setting it would change; for real it takes--executeand the server’s exact name, creates and edits with new IDs, never deletes anything the server has beyond the blueprint, and reads the server back to check.structure remapprints the ID table an apply wrote, without logging in. Export and diff need Manage Server; apply names every missing permission before its first step.role— a server’s roles.role listprints them highest first with the ones the bot holds marked and the bot’s top role named, because that is what every change is measured against: a bot can only act on roles below its own top one.role createandrole edit(name, colour, hoist, mentionable, the whole permission set as Discord’s own names) show what will change and wait for a yes;role deletedry-runs by default and for real takes--executeand the role’s exact name. Before any of them writes, a missing Manage Roles is named, a role the bot cannot reach is refused asHIERARCHY_DENIEDwith both positions, and a right the bot does not hold itself cannot be handed out. The tool never touches a role the bot holds.permission— who can do what in one channel or category.permission showprints each role’s overwrite by name;permission setchanges one role’s, merging into what it already has there (--allow,--deny,--clear), behind a preview and a yes.permission show --nameslists the permission names without logging in.member— the people in a server.member listis the same listingmembersprints, under the group name and with the same flags.member kickandmember bandry-run by default and for real take--executeand the member’s exact username typed back — there is no flag that answers that, so neither ever runs unattended — with--reasonrequired, because it is what the person sees and what the next moderator reads.member unban,member timeoutandmember nickshow what will change and wait for a yes. A timeout always has an end:--untiltakes30m,2h,7dor a date and time, and 28 days is as far as Discord itself allows. Before any of them writes, a missing right is named, and so is the reason Discord would refuse anyway: the server’s owner, whom nobody can moderate, the bot itself, or somebody whose top role is not below the bot’s. A ban deletes no messages — clearing messages is its own command, with its own gate.invite— the links into a server.invite listprints every one with its link, its uses and when it expires;invite createmakes one for a channel (how long it lasts, how many people may use it, whether membership is temporary) and prints the link once;invite revokedry-runs, and for real takes--executeand the exact code. The links appear in those two listings and nowhere else — a revoke names the code, and a link that turns up inside an audit entry is blanked, because a link in a log is still a working door.audit-log— Discord’s own record of who did what on a server, newest first, narrowed by--action,--userand--since. This is everyone’s changes, not just this tool’s; the tool keeps its own separate log of its own writes in your folder.webhook— the URLs that post into a channel.webhook listshows every one with the secret part of its URL blanked;webhook createmakes one after showing you what it will be, and prints the whole URL once, on your screen, and only if you asked for it with--reveal;webhook deletedry-runs, and for real takes--executeand the webhook’s exact name. A whole webhook URL is a password: whoever has one posts into that channel as anything they like, with no bot and no invite, until somebody deletes it. So it is the one thing the tool prints exactly once and never writes into its own output, its own log, or the--jsona script would keep.emojiandsticker— the pictures a server holds. Each lists what is there (an emoji listing shows the:name:you paste to use one), adds one from a file after showing you the file and its size, and removes one behind a dry-run and its exact name. Discord caps an emoji at 256 KiB and a sticker at 512 KiB and refuses a bigger one with an error naming neither the file nor the limit, so the tool measures it first and names both.automod— the rules Discord applies by itself, with nothing of yours running.automod listshows what each one catches and then does;automod createwrites one, where the flags say what it catches — words, a pattern, one of Discord’s own lists, too many mentions, or whatever Discord judges to be spam — and what it then does: block the message before it posts, alert a channel, or time the author out. Those three are the whole list, and none of them deletes anything.automod editchanges a rule inside the kind it already is, because Discord fixes that when the rule is made and never changes it;automod deletedry-runs, then takes the rule’s exact name.channel edit— what one channel actually is: its name, its topic, whether it is age-gated, its slow mode, where it sits in the list. It shows you the before and after, waits for a yes, and then reads the result back from Discord rather than telling you what it asked for. Only the fields you name are sent, one already at the value you asked for is not a change at all, and a field that channel type does not have is refused by name.watch— the one command that stays running.watch rules addwrites a rule — what it fires on, what narrows it, and what it then does — after showing you the whole thing and asking;watch rules list,edit,remove,enable,disableandtestmanage them, andtestruns a saved event past them and prints what would have fired without firing anything.watch runthen watches the server live until you stop it, andwatch status,watch stopandwatch reloaddrive a running one. What a rule can do is a short fixed list: alert you, tag or bookmark the message, record what came with it, sync that channel into the archive, or put its attachments and links in the review queue. Nothing downloads, sends a message of its own, edits or deletes. It needs a Mac or a Linux machine, and nothing is installed as a service — you run it in a terminal, or under whatever you already use to keep things running.schedule— a message posted later, by the watcher.schedule posttakes a time (--at) or a repeat (--every 1d, or a cron line);schedule listandschedule cancelmanage them. These fire only whilewatch runis up on that machine — Discord has no way for a bot to hand it a message to send later, so this is the honest version rather than a promise nobody keeps. Every listing says so on every row.send --atsays the same thing the waysendsays it, and carries no file and no reply: the message goes out later, when the file may have moved and the message it would have answered may be gone.event— something in the server’s own Events tab.event list,create,editanddelete. Discord holds these, so they happen whether or not your machine is on, and every listing says that too. Deleting one is the delete gate: dry-run, then--executeand the event’s exact name.clear-messages— clears one channel with--channel, or everywhere the bot can reach in a server with--server. Read before it sends or deletes anything first.delete— removes the container itself:delete channel,delete categoryordelete thread. Dry-runs by default, and deleting for real needs--executeand the target’s own name typed back.leave-server— the bot leaves a server. Nothing in the server is deleted; Discord gives a bot no way to delete a server it does not own, and a bot never owns one. Same two steps asdelete.bot— shows the active profile’s bot: username, description, avatar, intent, invite URL. Edits go behind a diff and a confirmation.
Reading the output with a script. Put --json before the subcommand and
any command answers with one object on standard output, with previews, prompts
and progress moved to standard error. The keys are the same whatever ran, so
there is one thing to parse rather than one per command, and the exit code says
the rest: 0 done, 1 stopped at a gate or only partly finished, 2 refused, 3 an
answer was needed and there was no terminal to ask on. --jsonl streams one
line per message, member, server or archive hit first, and closes with the same
object.
Every write also reports the permission it needed, the permission it held, and what it read back afterwards — and if it could not read anything back it says so rather than claiming the write worked.
The folder is yours alone. ~/.discord-tools/, its .env and the profile
records are written so only your user can read them, because that is where the
bot token lives. If that stops being true — a copy restored with the wrong
permissions, a folder shared onto a drive that does not keep them — doctor
says which file and which mode, and every command that writes to Discord
refuses until it is fixed. Reading still works, so you can find out what is
wrong. Your exports are not part of that check; they are yours to share.
Before it sends or deletes anything
The two commands that cannot be undone are the two that ask the most.
Clearing messages dry-runs by default. clear-messages with no extra flags
counts what it would delete and stops. Deleting for real takes two separate
things: the --execute flag, and typing DELETE at the prompt. Nothing is
deleted between those two steps. telegram-tools only ever touches messages —
never the topic, never its ID — and checks you actually hold the
delete-messages permission before it starts. discord-tools clearing a whole
server includes threads and forum posts unless you pass --skip-threads, and
reports anything it had to skip.
Deleting messages is the same gate on a selection. Both tools’
message delete lists every message it would remove — by id, or every archived
match of a query — and stops; --execute then asks you to type DELETE, and a
selection above 200 is refused rather than trimmed until you raise the limit
yourself. There is no way to skip that prompt.
Nothing is downloaded until you say so. Attachments and links the archive
sees wait in a review queue, listed without contacting any host. Fetching one
takes a y/N at review approve, which has no --yes and stops with
APPROVAL_REQUIRED when there is no terminal; the file then sits in quarantine,
checked, until a second y/N at review accept — which shows the verdict
first and refuses a file a check blocked or a scanner flagged.
Sending shows you the message first. send prints the whole message and its
destination and waits for a yes, and so does every message verb on either tool —
a reply, a reaction, a pin, a forward — after showing the message it is about to
act on. On Discord a post pings nobody unless you pass --mention, and
--mention everyone asks even when you said --yes. The --yes flag skips that prompt, and because
a skipped prompt means nobody saw where the message was going, it only works for
destinations you named in advance — TELEGRAM_SEND_ALLOWLIST for one tool,
DISCORD_SEND_ALLOWLIST for the other. With neither set, every unattended send
is refused.
Creating and editing ask too. create, bots and bot show what they are
about to do — a diff, for an edit — and wait for a yes.
Applying a blueprint is the delete gate, used to build. structure apply
dry-runs by default and lists every topic, role, category or channel it would
make and every setting it would change. For real it takes --execute and the
chat’s exact title or the server’s exact name typed at a terminal — there is
no --yes, and with no terminal it stops with APPROVAL_REQUIRED — and it
never deletes anything the chat or server has beyond the blueprint: those are
reported as left alone. Each step it makes is written to the audit log, and on
Discord every step carries the audit reason too.
Roles explain the refusal before the change. On Discord a role write can be
allowed and still impossible: a bot acts only on roles below its own top role,
never on a managed one, and can only hand out rights it holds. role and
permission check all three before writing and say which one stopped it and
what would fix it, instead of relaying a bare 403. Deleting a role is the
delete gate — dry-run, then --execute and the role’s exact name, no --yes —
and any change that grants or removes Administrator is typed the same way.
The tool never edits the bot’s own roles, so it cannot raise its own rights.
A webhook URL is printed once and kept out of everything else. Anyone
holding one posts into that channel as anything they like — no account, no bot,
no invite — so discord-tools prints a whole one on exactly one screen, when you
ask for it with --reveal, and blanks the secret part of it everywhere else:
in the listing, in the --json a script reads, in the arguments it echoes
back, in its own log, and in the preview it shows before deleting one. Without
--reveal a new webhook’s URL is not printed at all, and the command tells you
where to find it. There is no flag that hands a script a working URL.
Removing a webhook, an emoji, a sticker or an AutoMod rule is the delete
gate. Each dry-runs by default and for real takes --execute and that
thing’s exact name typed at a terminal, with no --yes. Deleting a webhook
stops everything posting through it, and Discord cannot bring the same URL
back; a removed emoji leaves every message that used it showing a hole; and a
deleted AutoMod rule means the server has quietly stopped filtering what that
rule filtered, with nothing to announce it. Adding an emoji or a sticker,
writing or changing a rule, and editing a channel’s settings each show you what
they will do and wait for a yes.
Banning someone, or taking their rights away, is the delete gate on a
person. telegram-tools’ member ban and admin demote dry-run by default
and show who would be affected; for real they take --execute and the person’s
exact label typed at a terminal — their @username, or their name — with no
--yes and no way to run without a terminal. Every other admin change (a
promotion, a mute, an approved join request, a new or revoked invite link, slow
mode) shows who acts on whom and waits for a yes, and none of them has a
--yes either. Before any of it, the tool asks Telegram what rights your
account holds and refuses by name when the one it needs is missing, or when you
hold it but cannot use it on that person: an admin gives only the rights it
has, and cannot edit an admin who has more.
discord-tools’ member kick and member ban work the same way: dry-run,
then --execute and the member’s exact username typed at a terminal, with no
--yes and a reason you have to give, because that reason is what the person
sees and what the next moderator reads. A timeout, a nickname change, lifting a
ban and making an invite each show who is affected and wait for a yes.
Revoking an invite is the delete gate again — the exact code typed back —
because the link stops working for everyone holding it and Discord cannot bring
the same code back. Before any of it, the tool asks Discord what the bot holds
and refuses by name when the right it needs is missing, or when it holds the
right and still cannot use it on that person: the server’s owner, whom nobody
can moderate, the bot itself, or somebody whose top role is not below the
bot’s. A ban deletes no messages — that is clear-messages, and it has its own
gate.
Downloading asks twice, and nothing downloads on its own. Neither tool
fetches a link or an attachment because a sync or a rule saw it: it notes it
in the review queue and stops. review approve shows you the link exactly as
it was written, or the file’s name, type and size, and waits for a yes before it
fetches anything — and only then does it follow a link’s redirects, one careful
step at a time. What it fetches goes into a quarantine folder and through eleven
checks: the address must be a public one, the file must be what it claims to be,
an archive must not be a bomb, and a local ClamAV scans it if you have one. Then
review accept shows you the verdict and waits for a second yes before the file
is kept. A file the checks refused cannot be accepted at all; a file no scanner
looked at is marked UNSCANNED, and it is your call. There is no --yes for
either step, nothing is ever uploaded anywhere to be checked, and both steps
refuse to run when nobody is at the terminal.
Nothing a rule does can be undone, because nothing a rule does changes anything. The list of things a rule may do is fixed in the code and closed: alert, tag, bookmark, record what arrived, sync a channel, queue something for review. There is no rule that deletes a message, edits one, sends one of its own or downloads a file — a rule asking for any of those is refused when it loads, not when it runs. An alert to a channel goes out through the same send path as everything else, so it can only reach a channel already on your allowlist; an alert that runs a command of yours is checked against your PATH the moment the rule is saved, rather than failing quietly at three in the morning. Every alert carries a marker, and an alert arriving back from another bot is ignored, so two watchers cannot talk each other into a loop.
A scheduled post is checked when you write it, not when it fires. The
watcher posts with nobody looking, so a schedule post aimed at a channel that
is not on your allowlist is refused there and then. A time already in the past
is refused before you are asked to confirm it. And if the machine’s clock jumps
— a laptop waking up, a timezone change — a missed post goes out once, marked
late, rather than once for every hour it slept through.
The menu is never a shorter path past any of this. It has no equivalent of
--yes at all, its clear screen still dry-runs first and still asks you to type
DELETE, and backing out of a half-written message asks before throwing it
away.
Where your files live
Everything either tool keeps is in one folder per tool, in your home directory.
~/.telegram-tools/— the.envwith your API key;profiles/, one folder per login holding that account’s saved session and a small record of its name;archive.sqlite, the local archive;quarantine/, where an approved download waits for your verdict, andmedia/, where it goes once you accept it;exports/, where an export lands when you give--outputa plain file name; andaudit.jsonl, a line per message sent, chat created, topic cleared, thing deleted or download approved, accepted or rejected. That log is a local record for you, it holds no keys or phone numbers, and you can delete it whenever you like. The folder and everything in it are written so only your user can read them — if that ever stops being true,doctorsays so and every command that writes refuses until it is.~/.discord-tools/— the.envwith your bot tokens, readable only by you;exports/, where an export lands when you give--outputa plain file name rather than a full path;rules/, one small file per watch rule, which you can read and edit by hand;runner.lockandrunner.log, which say whether a watcher is running here and what it has been doing; andaudit.jsonl, a line per message sent, channel created, channel cleared or thing deleted. It is a local record for you, it holds no tokens, and you can delete it whenever you like. Discord keeps its own record too: changes made through this tool appear in the server’s audit log naming the command that made them.
There is no server, no account, and no third party. Nothing leaves your machine
except the calls to Telegram and Discord that you asked for, and neither tool
ever prints a token or a key — doctor will tell you a token is present and
working without showing you what it is.
Both tools are MIT licensed, and the code is on GitHub: telegram-tools and discord-tools.