Historial de cambios
Notas de versión de WhatsApp CLI: nuevas funciones, cambios y correcciones.
Notable changes to @wirecat/whatsapp-cli. One section per version, newest first; versions follow
semantic versioning, so before 1.0.0 the command interface may still change.
0.8.0 — 24.10.2026
Added
-
The owner-run
bin/live-oneshot-dropcan send its two test messages automatically with--sender-profile, using two explicitly confirmed accounts owned by the same person. -
Session and setup receipts declare their actual provider schemas. The complete contract audit checks them alongside shared entities.
-
Preview a group or channel invite with
chats inspectwithout joining it. -
account sessions listshows linked device ids and marks this device. Names and last-active times are unavailable. -
chats folders listandshowread custom WhatsApp Lists and their chats from synced label state. -
Incoming community announcement comments are decrypted into their post's stored thread;
messages commentsreads the comments this device has received. Channels have no comments, and comment sending stays unavailable.
Changed — may break callers
-
The one-shot regression checks the receiver's store before restarting
serve; delivery only after restart is inconclusive. Lost messages and inconclusive checks now return nonzero. -
Template lists use a query instead of a name flag. Replace
bot templates list --name hello_worldwithbot templates list name:hello_world. Meta applies the name predicate before paging; unsupported query fields are refused before a request. The retired flag has no compatibility alias. -
Shared CLI contracts require explicit provider overrides. Discovery includes the inherited definitions, schemas, reasons and exact deliberate differences. Contact activity filters with
scope:andat:, and diagnostic run searches withstarted_at:. Unknown roster and webhook type lists are omitted instead ofnull.
Fixed
-
Permission-denied commands stay discoverable in CLI and MCP with current availability and reasons. Calls still enforce permissions; a listed command grants no execution access.
-
The owner-run one-shot regression can pause and restore a persistent systemd user service, including one that selects its profile through its environment instead of a command argument.
-
Full settings sync recovers stars and deletions as history updates, instead of presenting them as new live events.
-
The installation guide explains how to allow the optional agent-skill hook with npm's scoped
--allow-scriptsflag.
0.7.0 — 24.10.2026
Added
- The first command at a terminal asks once for your languages, other WireCat tools to install, and whether commands and agents may only read. Pipes, agents and noninteractive runs skip the questions.
- Preview a group or channel invite with
chats inspectwithout joining it. account sessions listshows linked device ids and marks this device. Names and last-active times are unavailable.chats folders listandshowread custom WhatsApp Lists and their chats from synced label state.- Incoming community announcement comments are decrypted into their post's stored thread;
messages commentsreads the comments this device has received. Channels have no comments, and comment sending stays unavailable.
Changed — may break callers
- Live JSON output uses item and end records.
watch --jsonlwraps each message or event in{ "type": "item", "data": … }, uses snake_case fields and finishes with atype: "end"record. Update scripts that consumed bare messages or events. - Diagnostic search is
search runs. Replaceruns searchand its filter flags with query terms such asstatus:failed,error_code:timeout,operation:messages.downloadandprofile:work. - Duplicate-task lists require
tasks duplicates list. The bare group shows help. - MCP exposes more writes through permission keys. Commands that change future permissions or automation
require
mcp.authority: allow; raw Cloud API calls requirebot.api: allow. Both default todenyover MCP. Store maintenance and other writes follow their command permissions. - Shared commands use canonical names without compatibility aliases. Use
contacts info|events|aliases,chats links,metadata show|sync,tasks update,commands info,mcp add|checkanddoctor reports show|create. Models usemodels list,models downloadandmodels keys.
Fixed
- Commands to end other sessions or remove contacts are hidden from help, discovery and MCP because a linked WhatsApp device cannot perform them.
- The agent skill describes explicit read receipts correctly. Reading changes no read state;
chats mark-readclears the unread mark and sends the receipt. The top help points to linking examples without promising Windows instructions that setup help does not contain. - Full settings sync recovers stars and deletions as history updates, instead of presenting them as new live events.
- The installation guide explains how to allow the optional agent-skill hook with npm's scoped
--allow-scriptsflag.
0.6.0 — 24.10.2026
Added
wa <profile> bot: a WhatsApp Business number on Meta's Cloud API. A bot profile keeps an access token in the system keychain (bot auth set) and the number's ids as settings (config set --bot phone_number_id …).bot menames the number;bot api <operation>runs any of 63 Cloud API operations generated from Meta's OpenAPI file, and asks before the ones that cannot be undone, such asregister-phone-number. It never links a device or starts the engine. What to watch for: never register a number whose WhatsApp you use on a phone — that deletes its account.- A bot sends text, replies, files and templates.
bot messages sendwrites to a phone number in digits or touser:<id>; a file goes up once and is sent by its media id.bot templates listandbot templates send <chat> <name> [language]cover the approved messages, the only kind WhatsApp delivers more than 24 hours after a person wrote.bot media download <id> --output <file>saves a received file. WhatsApp has no silent message, edit or delete for a bot, so those are refused. - A bot receives messages by webhook:
bot watch --public-url <tunnel address>. Meta never lets a client poll, sowalistens on127.0.0.1:8766behind your tunnel, answers Meta's subscription check with the verify token, refuses any delivery not signed with your app secret, and acknowledges a delivery only once it is in the local store. Keep both withbot auth set --secret app-secretand--secret verify-token.bot webhooks set <url>points this number's webhook at the address. Delivery receipts arrive too. With the store holding a person's last message, a free-form send more than 24 hours later is refused before it leaves. bot mcpserves the bot's commands to an agent, as tg and max do, and docs/bot.md walks through Meta's test-number setup, the tunnel and each command.- Verified media backups.
attachments download <dir>archives available files across chats with size and SHA-256 manifests;attachments check <dir>verifies them offline.attachments jobsmanages opt-in Linux daily timers. Completed copies survive interrupted runs and message-store deletion; interrupted files restart.attachments move <dir> <archive-dir> --before-time <time>moves older images into another verified archive, retaining relocation records so checks follow the copies and later downloads do not duplicate them. - Saved voice-note transcripts match word forms and spellings that speech recognition may have garbled.
text:searches written text only;exact:disables expansion for that word. - cli-core 0.394.0. Store and statistics commands gain examples, and
contacts preparegives a cited brief before a conversation. - Upgrades are checked against saved state from version 0.2.0. Offline validation uses a synthetic profile to check that configuration, stored messages and the device database location survive an upgrade.
- Pull requests cannot grow the command-lint baseline. New violations must be fixed or carry a dated, reasoned override, so adding a feature cannot silently weaken the existing checks.
Changed — may break callers
- Shared flags use one meaning. Analysis uses
--analysis-provider, vectors--embedding-provider, consent removal--consent-provider, tag ownership--claimand stored-answer removal--answer-model. Contact notes read--text-file; group message lifetime is--disappear-after. Catch-up views use query terms such asinbox kind:group. Old CLI flags remain hidden aliases for one compatibility release and cannot be combined with their replacements; saved MCP requests must use canonical arguments. - MCP message deletion takes
message, chat export takeschat, and duplicate-task operations taketaskand, for a pair,other_task. Update saved MCP requests usingmessages,chatsordup. CLI positional arguments keep their order. - Command discovery (
commandsandcommands schema) rejects--jsonl; use--jsonfor its single document. - Read commands no longer send read receipts.
messages list,inboxandreviewdrop--mark-readand--no-mark-read; removecatch_up_marks_readfrom configuration and usechats mark-readexplicitly. - Misspelled permission keys now stop the command. Correct or unset keys such as
messages.sednbefore running. - A mistyped profile lists the real profiles. An unlinked profile's login hint now points to
setup. - Setup returns suggested commands in
item.commandsand login details initem.session. Update scripts that readitem.nextoritem.device;nextis reserved for structured continuation steps.
Fixed
- cli-core 0.399.0 supplies the shared flag migration, additional help examples, offline review and the updated MCP server dependency. The command-lint baseline drops from 52 entries to 38; guides, skill and the generated reference use the released flags.
- Message-search commands no longer list or delete saved meeting searches.
- Command corrections preserve the chosen profile; help shows concise descriptions and concrete examples.
- More useful command discovery. cli-core 0.396.0 adds shared examples, concise skill summaries and profile-preserving corrections for joined command paths; cli-base 0.50.2 recognizes the approved messenger vocabulary and flag roles. Command discovery uses
--jsonand refuses--jsonl. - Input errors name the selected command's flags. WhatsApp adapters report domain fields for unsupported invite-link filtering, conflicting name fields and privacy refusals; cli-core formats those fields at the CLI boundary.
- CI scans the checked history. An unrelated fetched branch no longer fails a clean pull request's secret check; findings in the checked commit or any ancestor still block it.
- A ban's end is shown in the local time zone and names that zone. Stored timestamps remain UTC.
- Unexpected engine failures use the provider error code. Known validation and messenger errors keep their codes.
- Agents are no longer offered channel comments, which WhatsApp cannot read (cli-core 0.393.0). A direct call fails before connecting. Commands also show more concrete examples in their help.
Security
- The engine uses the patched Go certificate verification library. Release and monthly checks now audit reachable Go vulnerabilities, and dependency updates cover the engine's Go module.
- Local releases run engine tests, the coverage floor and security checks before publishing. The npm token is supplied only to publishing, and publishing skips lifecycle scripts because validation already built the package.
0.4.0 — 22.10.2026
Changed — may break callers
- A call with no
outcomemay have ended. Before, a call message with nooutcomeinprovider_metadatawas always still ringing. Now a call that ends for a reasonwadoes not know is kept as "Voice call" with nooutcome, so it does not pass as missed or answered. A script that reads a missingoutcomeas "ringing" should read the text as well: a ringing call's text starts with "Incoming".
Fixed
- A call answered or declined on your phone is stored as answered or declined, not missed. While
serveorwatchruns, WhatsApp tells a linked device only that the call was taken or turned down on another device, andwaread every such end as missed. Now a call you answer on the phone is "Voice call" withoutcome: answered, one you decline is "Declined voice call", and one that rang out or that the caller hung up before you answered is "Missed voice call", as the phone shows it. An answered call taken on the phone has noduration: this device does not hear when it ends, andwano longer stores0for it. A call that rang whileservewas not running, and whose end the reconnect does not bring, is stored as missed instead of staying "Incoming voice call".
0.3.0 — 22.10.2026
Added
- Date filters say how they read the date (cli-core 0.384.0). The JSON answer of a command with a date filter,
such as
contacts context,contacts timeline,messages sync,chats moderateandstore export, givesmeta.windowwith thesinceanduntilit used, in UTC, andmeta.timezonewhen a date you typed had no zone and was read in the local one. - cli-core 0.382.0. The first run opens the local store as version 28, which adds columns and removes nothing.
- Your messages show whether they were delivered and read. In a one-to-one chat, a message you sent carries
delivery:deliveredonce it reached the other person's phone,readonce they opened it. It comes from WhatsApp's receipts, whichserve,watchand any other command keep, and from the history push. It only moves forward. A group's messages have none.watch --eventsprints each receipt. Needs cli-core 0.382.0 (store version 27). The engine speaks protocol 44. - A chat's mute, archive and unread mark from the phone are kept (cli-core 0.382.0). While
serveorwatchruns, and from the history push, the local store saves whether a chat is archived, muted and until when, and marked unread. Soinboxandreviewover the store leave out muted and archived chats, andinboxlists a chat you marked unread. A chat read on the phone loses its unread mark, and the store now counts each new message from someone else after that read, soinboxshows a chat again once a new message arrives.chats showshowsmuted_untilandmarked_unread. What to watch for: a read on the phone resets the chat's last-message time to the newest message the phone read. Engine wire change:mutedUntilandmarkedUnreadon a chat. Engine protocol 45. - Calls to you are kept as messages, so
inboxandreviewshow who called. Whileserveorwatchruns, a call becomes a message in the caller's chat: "Incoming voice call" as it rings, then "Missed voice call", "Declined voice call" or "Voice call" when it ends. It carries"action": "call"inprovider_metadata, as tg marks its calls, so chat stats do not count it as something said. A group call is kept as it rings.wanever answers or declines a call. The engine speaks protocol 42.
Fixed
- A store the pre-rename
cli-messagingwrote is refused again, and left untouched (cli-core 0.385.0). Since cli-core 0.382.0 such a file was opened and failed withinternal_error; it now fails withconfiguration_error. chats listshows the groups you are in. WhatsApp has no chat list, so the store knew only the groups a history push or a message had named, andchats list kind:groupcould come back empty. Every connection now saves the groups you are in, with their names and member counts; runserve(or any command that connects) once for them to appear.- A chat is found by a name copied with its emoji, by part of its name and whatever the case. "Семья❤️" copied
from WhatsApp carries an invisible mark after the heart, and was refused as "no chat by that name or id" while
"Семья❤" worked. Names now match without that mark, ignoring case, and by part of the name, as
waalready finds a person (cli-core 0.381.0). A name that matches several chats is refused as before; the refusal now reads "N chats match that name". What to watch for: a part of a name that used to be refused may now name a chat, when exactly one chat matches it. - Reactions that other people send are stored (cli-core 0.381.0).
serveandwatchkeep each person's newest reaction, live and from the history push, andwatch --eventspasses each one on as aperson_reactionevent. A message then shows its reaction counts and your own reaction, from the phone, inreactions. A reaction taken back is dropped, and an older history push no longer undoes a newer change. What to watch for: a reaction you send withreactions addstill goes to WhatsApp only, not to the store. Engine wire change: areactionevent and a history push'sreactions. Engine protocol 43. stats chats retentionandchats stats showread--since-time/--until-timein their--timezone, and a date alone in retention's--until-timecovers that whole local day (cli-core 0.380.0).stats charts,contacts timelineand the MCPstatstool name the zone inmeta.timezoneand give the UTC window inmeta.window.- A chat you read on the phone stops counting as unread. While
serveorwatchruns, reading a chat on the phone sets its unread count in the local store to 0, soinboxno longer shows it. Before, the count stayed until the next history push. What to watch for: new messages do not raise a stored count, here or in any other chat, so until the next history pushinboxfinds them only with--new. Archiving, muting, pinning, marking a chat unread, deleting or clearing a chat on the phone are not saved yet, soinboxstill lists an archived or muted chat.watch --eventsprints none of these changes, as intg. - The ban error says its end time is in UTC. "the ban ends 2026-10-21T14:03:00.000Z" now ends in "UTC", so it is
not read as local time.
standing.untilis unchanged.
Changed — may break callers
- A typed date or time is read in your zone, and a date alone covers your whole local day (cli-core 0.379.0).
--since-time 2026-10-01used to start at UTC midnight; it now starts at local midnight, and--until-timewith a date alone ends at the end of that local day. A time without an offset, such aschats mute --until 2026-10-09T09:00, is read in the command's--timezonewhere it has one, else in the computer's zone. A value withZor an offset means what it says, as before. What to watch for: a script that passed a date alone and expected the UTC day now gets the local one; pass2026-10-01T00:00:00Zto keep the old window. Stored and printed times stay UTC.
0.2.0 — 21.10.2026
Changed — may break callers
-
--qr-fileis gone: without a terminal,session startandsetupprint the QR code as text for an agent to paste into the chat, instead of a PNG the person opens in an image viewer. The text is plain half blocks, no colours, about 73 columns wide. Each code is followed by a note telling an agent to paste it, and with--jsonit is also one stderr line,{"event":"qr","qr":{"text","expires_at","scan"}}. A QR link no longer needs a terminal; a pairing-code link (phone) still does, and without one says that it asks questions. A script that passed--qr-filenow fails with an unknown option. In a terminal the code is drawn as before, as in tg and max. -
A one-to-one chat typed by its phone number is addressed by its LID, the id WhatsApp files that chat under, when WhatsApp has already given it. WhatsApp answers a send by the number with the LID, so a later
messages delete,messages star,reactions addorpolls voteby the number could miss the message: the deleted text stayed searchable, the star was lost, and the vote made a second chat. Every command that names a chat now looks the number up in the LIDs this device knows; nothing is asked of WhatsApp and nothing is recorded. A number with no known LID, a group, a LID or a channel is used as typed. An--offlinecommand reads the store by the id as typed, and finds a chat that moved from that number to its LID once the store recorded the move (see Added). Engine wire change: acanonicalChatrequest. Engine protocol 38. Two things the owner may notice:- A recipient list entry added by the phone number stops matching once that number has a LID, and a send to it
is refused with exit 7. The refusal names the id to allow: run
recipients addwith it, or with the number again, which now saves the LID. An entry added by the number moves to the LID by itself at the nextserveorwatchstart, when this device already knows the number's LID, whether or not the store holds a chat under it (cli-core 0.356.0). The number is not asked of WhatsApp and is not recorded. Engine wire change:recipientson thestoredChatsrequest. Engine protocol 40. - A chat the store already holds under the phone number, such as one wa 0.1.0 saved, moves to its LID on the spot
(cli-core 0.356.0): the first command that names it by the number, a send,
delete,staror reaction included, moves the stored chat, its messages and its recipient-list entry to the LID, then acts on the LID chat. An--offlinecommand moves nothing; it finds such a chat by the number once the store recorded the move.
- A recipient list entry added by the phone number stops matching once that number has a LID, and a send to it
is refused with exit 7. The refusal names the id to allow: run
-
wa --helpand a barewashow a short first screen instead of every command and option: whatwais for, the usage line, the other WireCat tools it works with, and where to start.wa --help-allprints the old help andwa help optionsthe options of every command. Each command's own--helpnow ends with what it returns, the commands that usually come next, and an example. A barewa commandsprints a text list by section, also when piped; a script that read its JSON must pass--json(cli-core 0.344.0). -
chats mark-readtakes a mark-unread off, and needs WhatsApp's settings keys for it. It sent a read receipt only, which leaves a chat marked unread on the phone, and in your own chat it sent nothing at all. It now first marks the chat read on your devices, up to--until-idor the newest message the store holds, then sends the receipt as before. The same goes for--mark-readonmessages list,inboxandreview, one more write per chat. Right after linking, before WhatsApp has sent the settings keys, these now fail withsettings.keys-missing(safe to retry) and send no receipt, where they used to send it; keepserverunning a few minutes and run them again. Engine protocol 34. -
The MCP server's
wa_readandwa_writetake each command's own arguments, in snake_case.search messagestakesquery(nottext) and noast;polls createtakesquestion;wa_tools_searchshows each command's fields. The agent is offered every command the profile's permissions allow, including some it was not offered before; marking read,--as-ownerand--allow-any-filestay off MCP. An agent's runs are namedmcp <command>in run history and the send journal.conversations rebuildreplaces the agent-onlyconversations_refresh. -
A global
npm installno longer writes the agent skill into~/.claude/skillsand~/.agents/skillson its own. npm hides what an install script prints, so the files appeared without a word.wa personal setupstill offers the skill, andwa skill installinstalls it; setWA_INSTALL_AGENT(all,claude,codex,cursororgemini) to have the install do it as before. The Windows installer is unchanged: it says what it installs. -
session start phoneandsetup --method phonerefuse a number without+and the country code (exit 2,input.invalid). WhatsApp reads the first digits of the number as the country code, so a number typed the way it is dialled at home asked for a pairing code on someone else's account. Nothing reaches WhatsApp until the number starts with+. -
messages deletewithout--for-everyonedeletes the message for you only, where it used to be refused (exit 2).waasks WhatsApp to take the messages from this account's devices, to leave them with the others in the chat, and to keep the photos and files the phone saved from them. Each message must be in the local store, since WhatsApp names it by its sender and the time it was sent; one the store lacks is exit 6 and nothing is deleted. A device without WhatsApp's settings keys yet answerssettings.keys-missing, aschats mutedoes. An agent'smessages deleteover MCP, which was refused too, now does the same, for the owner only; deleting for everyone stays the owner's--for-everyone. A deletion asks first by default, and over MCP that level lets the agent's call through, so at the default an agent can now delete the owner's copy. To keep agents from it, setpermissions.messages.deletetoreadonlyordeny: the MCP server then does not offer the command, and an agent's call is refused withpermission_error(access.denied), as the owner's ownmessages deleteat those levels is, with exit 5. A delete of several messages that fails partway, with or without--for-everyone, now names the ones already deleted and is not marked safe to retry: delete only the rest. A message you delete for yourself on the phone leaves the local store the same way whileserveorwatchruns. Checked live on the phone for your own message in your own chat; a message in a group or in a one-to-one chat with someone else, and a delete made on the phone, are not checked yet. -
messages downloadasks your phone for a file WhatsApp no longer keeps, as WhatsApp's apps do, instead of answeringnot_found(exit 6) at once. It waits up to 20 seconds for the phone to upload it again and fetches the new copy. A phone that does not answer ends the download withtimeout(exit 9); exit 6 now means the phone no longer has the file either. A script that read exit 6 as "expired" must also expect exit 9, and give a slow phone a longer--timeout. A new copy that then fails to download isprovider_error(exit 11,media.retry-failed), whatever the first failure's code was; when the phone answered an error but still gave a path, it is the phone's answer instead (see Fixed). The store keeps the new copy's address, so a later download or forward uses it and does not ask the phone again (see Fixed). Known limitation: a file WhatsApp no longer keeps does not download yet. On the phones tried, the phone answers that it uploaded the file again, but it encrypts the new copy with a new key that it does not send to linked devices, sowacannot read it. The download then ends withmedia.retry-failed("the phone uploaded this file again, but the new copy did not download") and saves nothing, rather than a broken file. A file WhatsApp still keeps downloads as before. For an old file, open it on the phone, or ask the sender to send it again. -
messages forwardsends a stored place or contact card, where it used to refuse it. The store now keeps a place's coordinates, name and address, and a contact card itself, next to a file's keys, where no answer shows them. A place or card stored before this, a live location and several cards in one message are still refused. -
A person's chat moves to their LID when WhatsApp gives them one. WhatsApp names a one-to-one chat by the person's phone number until it gives them a LID, its private id for a person.
serveandwatchnow move the stored chat and its messages to the LID, somessages listandsearchshow one chat, and no chat is left under the phone-number id (…@s.whatsapp.net). A script that saved that id must take the new one fromchats list. The move is repeated at each start for 30 days, so one that came while only a one-shot command ran still lands. A send typed to the old id stores a chat under it again, which a later repeat of the move merges. The merged chat keeps the stricter privacy of its two forms: hidden from search if either was, personal if they disagree, and a scope pinned on either stays pinned (cli-core 0.353.0). A chat stored under a phone number by wa 0.1.0 moves too:serveandwatchhand the stored chat ids to the engine as they start, and one whose LID WhatsApp already gave moves at once (cli-core 0.350.0,storedChats). If that fails,wasays so once on stderr (stored_chats_failedin the run record) and goes on. Engine wire change: astoredChatsrequest. Engine protocol 37. -
serveasks WhatsApp for the LID of a chat still in phone form, so that chat moves too. It asks a minute after history stops coming orserveorwatchstarts, for chats the account already has only: five people per request, one request every 10 seconds, at most 20 people per run, and each person at most once a week. The owner's ownlookupkeeps its own pace. If WhatsApp asks to slow down, the lookups stop for a week and the next write is refused withaccount.rate-limited, which holds the profile's writes for 15 minutes, also whenserverestarts meanwhile. Not yet run against WhatsApp. -
A command cut off by a dropped or ended connection no longer answers
network_error(exit 10), which invited a retry and a double send. A send, edit, reaction, group change or other write whose request left before the network dropped, and could not be sent again, now answersoutcome_unknown: exit 14,retry.safe: false, also when WhatsApp handed the login to another connection meanwhile, since the write may have gone through. Check the chat before you send again. Otherwise, a command relayed throughservewhile another process took the login over now answersdevice.replaced, and after a ban or a logoutaccount.bannedordevice.logged-out. A script that retried onnetwork_errormust not retry these. -
chats rules setrefuses anewAccountrule withvalidation_error(exit 2) and saves nothing: WhatsApp does not say when an account was made, so the rule was saved and could never match. A script that sets one must drop it. -
A send that waits on another
waprocess exits 9, not 1 (cli-core 0.338.0): when another process on the profile is sending or clearing the pace for longer than the 5 s wait, a send orflood clearanswerstimeout, since nothing was sent and a retry is safe. A change to the recipient list held elsewhere for 5 s is refused with exit 9 and the list stays as it was; two changes at once no longer undo each other. -
WA_READ_ONLYrefuses more, always with exit 5 and "this process is read-only (WA_READ_ONLY)" (cli-core 0.328.0 to 0.337.0). An agent that did any of these under the lock must do it in a process without it. Newly refused:session start,session endandsetup, before they act ("it cannot log in or out");session startandsetupon a profile linked before used to go ahead;- the message store:
store reset,restore,clear,migrate,repair,decryptandcopies delete(--dry-runstill previews); - what the owner saved:
searches create|delete|clear,contacts alias set|rm,contacts notes add|edit|removeandtags remove; - other local writes:
tags add,tags auto,mentions confirm|reject,contacts link|unlink,attachments text set,chats tracking add|remove,conversations links add|clear,conversations consents revokeandtasks reject;tasks closeonly suggests the close; - and their MCP tools.
messages deleteunder the lock exits 5, not 7 ("asks before it acts").wa upgrade --checknow runs under the lock;wa upgradewithout it is still refused. -
conversations embedandconversations buildcount as writes to an outside service, andsearch conversationsas a read from one (cli-core 0.334.0, 0.336.0): a failed run is not marked safe to retry, and underWA_READ_ONLYeach refuses before it sends chat text or the query to a remote provider; a local model still works. They always run in the calling process, never throughserve. Their help now says "Changes something in WhatsApp", which here means the outside provider. -
A ban or rate limit holds every write, for as long as it lasts (cli-core 0.334.0): while WhatsApp has the account banned or rate-limited, a reaction,
chats mark-readand a group title change are refused with exit5too, not only sends and edits. Reads still work;inboxandreviewwith--mark-readstill show what they read and warn that nothing was marked read. A temporary ban WhatsApp dates holds writes until its end, the error says when, andstanding.untilis that time. A ban with no end hasstanding.statebanned, notfrozen, anduntil: nullindoctor --json,flood clear --jsonandserver status --json, where it held for a year before;wa <profile> flood clearor adoctor --onlinethat connects lifts it. -
servebanned by WhatsApp is not restarted by its unit (exit 5 joins exit 4 in the unit's no-restart list): a restart 30 s later connected again during the ban.wa <profile> server statussays it stopped and will not restart; start it again withwa <profile> server startonce the ban ends. -
An older client's
session endrelayed intoserveis refused withpermission_error, exit5,sent: false, namingserver stop; it wasprovider_unavailable, exit12,device.in-use.serve's connection and the keys stay as they were. -
Ctrl+C or
--timeoutduring the history wait after linking ends the wait, not the command:session startkeeps what arrived, counts it and exits 0. It exited 130 or 9, though the device was linked.setupgoes on to its account, chat and agent steps, which get a deadline of their own (cli-core 0.334.0); a Ctrl+C after the wait ends setup (130). With--timeout, the link and the wait share that one deadline, and the steps after it get that long again. -
chats listandchats showanswer from the local store and never connect (cli-core 0.328.0), so they run here whileserveruns.recipients addruns throughserveinstead of being refused with exit 12. -
A link without a terminal waits 5 minutes by default, not 30 seconds (cli-core 0.328.0), so an agent's
session startis not cut off before the code is scanned.--timeoutstill decides when given. -
A command path given as one argument fails (cli-core 0.328.0):
wa 'chats list'printed the root help and exited 0; it now fails withvalidation_error. -
wa mcpoffers only whatwahas (cli-core 0.328.0, 0.335.0):chats link create, which failed, is gone, and a command that refuses--offlineno longer has anofflineinput; an agent that passes one getsvalidation_error.metadata getandmetadata refresh, which failed too, now work (see Added). -
chats eventsanswers from the local store (cli-core 0.336.0): it no longer refuses with exit2, never connects, and works with--offline. What it lists, and howstats chats showcounts members from it, is under Added. -
A chat or person named by name is found in the local store first (cli-core 0.337.0): part of the name is enough, and a name several stored chats match is refused with "… matches N chats", as
chats showrefuses it; a caller that matched the old "N chats share that name" must match the new text. Only a name the store does not hold is asked of WhatsApp's chat list, so a one-shot command now finds a stored group by its name.watchtitles a chat from the store and follows its renames; a chat the store has never held has no title inwatch. -
A live edit or deletion of a channel post is no longer passed on:
watchandserveused to hand it on under an id the store does not file the post by. A channel's posts are now read withmessages sync <channel>.
Security
- The engine log (
WA_ENGINE_LOG) no longer keeps media download links. When a download failed on the network, whatsmeow wrote the whole link into the log: the file's path on WhatsApp's servers, its hash and the signedoh/oevalues, which together fetch the file for anyone who has them; a failed delete of history-sync media wrote the mediaauthtoken the same way. Every link now becomes[url], a server path[path], and those values…, before the line is written. - A message sent to a phone number is saved in the chat WhatsApp answered, with that chat's privacy kept
(cli-core 0.356.0).
waanswers a send to a person who has a LID with their LID chat, but the message was saved under the number that was typed, in a second chat. Nowmessages send,polls createandmessages forwardsave it in the answered chat. A chat made for it takes the typed chat's search visibility and scope; a chat that exists already keeps the stricter of the two, so a chat hidden from search does not come back into search under its other id.wa <profile> sends listnames the answered chat beside the typed one when they differ. - A failed background
messages syncno longer writes a chat title or a name into its job file (cli-core 0.355.0). The file keeps the error's code and points atwa store jobs show <id>, which prints the path of the job's log. The full message stays in that log, which only you can read (mode0600). - A chat hidden from search no longer turns up in a search that does not name it (cli-core 0.353.0). The
messages by and about a person in
contacts context, the pictures lane ofsearch messages, and the meaning half ofsearch conversationsandconversations relatedleft it in, and the search answers listed it as not yet embedded. A sender-only lookup no longer counts its messages.wahas no command that hides a chat from search, so only a store edited by hand was affected. WA_READ_ONLYrefuses a write to WhatsApp before anything connects.messages send,edit,delete,reactions addand the other commands that change something on WhatsApp used to connect and find the chat first; now they exit 5 at once, and a broken login no longer turns the refusal into a network failure. A permission level ofreadonlyordenyon the command does the same (cli-core 0.343.0).
Fixed
- An album wa sends shows on the phone as one album. It showed as two messages, a photo and a video. Messages wa sends now carry a message secret and a reporting token (all but reactions and poll votes), as WhatsApp Web, Baileys and mautrix-whatsapp send them, and each file of an album points at the album by the chat id WhatsApp delivered it to (the LID when the chat was typed by its phone number). Checked on a phone.
- Photos and videos wa sends show on the phone. They never did, as an album or a forward either, while wa's own
linked devices received them: the message lacked the file's URL. Photos, videos, voice notes, documents and
stickers now carry it, as WhatsApp's own apps and mautrix-whatsapp send them. Checked on a phone. A message now
also carries the time its file's key was made, as Baileys sends it, and a document with a caption goes in the
wrapper WhatsApp's apps use. A file stored before this fix, or one the phone uploaded again, gets a URL built from
its path when it is forwarded. The URL is kept with the file's keys in the store and is never printed. Engine wire
change:
urlandmediaKeyTimestampin a file's ref. Engine protocol 41. - A chat id works in a query without quotes.
chat:100000000000002@lidandchat:[email protected]failed withquery.unsupported-operator, because the@was read as an operator; only the quoted form worked. Both forms work now (cli-core 0.375.0, cli-base 0.47.3). session endunlinks the device on WhatsApp's side, which it never did before. Since the first release the engine sent the logout without connecting, so everysession endstopped with "the connection to WhatsApp broke before it confirmed the change; it may have gone through — check first", sent nothing and kept the keys. If you deleted a profile's keys by hand after that, the device is still linked: remove it on the phone, in WhatsApp → Linked devices.session endnow connects first, then logs out. When WhatsApp refuses the device on connect (logged out, or removed on the phone), the keys here are deleted, the command exits 0 and says "WhatsApp had already dropped this device". A network failure before the logout is sent keeps the keys; a connection cut after it was sent still answers "may have gone through". No engine wire change.- A group the owner left is found by its name or any id when a command reads its kept history (cli-core
0.368.0):
store export(also--output-dirwith named chats),store status,messages context --thread,messages evidence, a search's--chatandchat:,metadata get,chats members,chats events,chats documents, stats, tags, tasks, conversations, embeddings, pictures and attachments, aschats showandmessages listalready did. Before, these refused a left group by every name and id.chats tracking addandstore gaps planstill refuse it. - More of
serve's warnings carry a code in its log, so a script can match the code instead of the sentence: a flood wait (flood_wait) or a long pace wait (pace_wait), both withcounts.seconds, a held account (account_frozen,account_limited,account_banned), a wait that could not be remembered (wait_not_remembered), a send missing from the send journal (send_not_journaled), a recipient list that could not be re-keyed or read, and the reply audience and template warnings. The log record holds no sentence, chat id or name; the sentence at a terminal is unchanged (cli-core 0.364.0). - The first
serveafter an upgrade hands its chat re-keys to the store together, up to 500 at a time, instead of one at a time. On an account with thousands of chats stored by phone number, that start announces a re-key for each, and every one was saved as a batch of its own. A re-key that arrives alone waits at most 0.1 s. The store saves them in short passes with a pause after each, so anotherwacommand run during that start no longer waits seconds for it; re-keys left whenservestops are one warning, and the nextserveapplies them (cli-core 0.359.0). - A group you left, were removed from, or that was deleted is marked left in the store, so
wa <profile> store clear --leftfinds it; until now it found nothing. The mark comes from WhatsApp's group notice whileserveorwatchruns, and from awa <profile> chats leavethat succeeds; achats jointhat gets in takes it off (cli-core 0.356.0), and so does WhatsApp's notice that someone added you back (cli-core 0.357.0); a group you had archived stays archived. A group left while nothing was connected is not marked. A group marked left drops out ofchats list;chats showshows it and says you left it, and its messages stay in the store and in search until you clear it. Engine wire change:membershipStateon a pushed chat,"left"(protocol 39) and"member"(protocol 40). setup's link step is headed "Link this device" and no longer asks you to scan a QR code before one is drawn. The line under the code says where to scan; a narrow terminal shows only the hint to widen it.- A terminal too narrow for the QR code no longer asks you to scan it (cli-core 0.350.0).
session startandsetupsaid where to scan in WhatsApp before the code; when the code did not fit, only the hint to widen the terminal followed. Now the scan line comes after the drawn code, and a narrow terminal shows only the hint. - A one-shot command now saves the history WhatsApp pushes to it. WhatsApp sends a history push to a linked
device once. A command such as
messages sendwith noserverunning received it and dropped it. It is now saved to the store like the messages queued for it, and the command waits up to 3 seconds at exit for a push still downloading (cli-core 0.348.0,listenHistory).session endends this quietly; when the phone logs the device out mid-command,wasays on stderr that history batches arriving were not saved. - A group taken out of its WhatsApp community no longer names it in the store.
wasaves WhatsApp's notice that a group left its community, or that the community was deleted, and clears the group'sparent_chat_id; its title, unread count, member count and last activity stay. Each followed channel's push at connect, and any chat whose source does not say, no longer erases a stored unread count and last activity either (cli-core 0.348.0). Engine wire change:unreadandlastMessageAtmay be absent, andnameandparticipantstoo, on that notice. Engine protocol 36. - A group added to a WhatsApp community names it in the store at once.
wasaves WhatsApp's notice that a group joined a community and sets the group'sparent_chat_id, keeping its title, counts and last activity. Before, the parent showed only after a live read of that one group. session startno longer gives up right after the phone confirms the link. WhatsApp restarts the connection once pairing is done, and wa took the start of that restart for a failure: it said "linked, but could not reconnect to WhatsApp in time" and exited 10, the device linked but the history never received, while the phone's linking screen hung waiting for it. wa now waits for the new connection, up to a minute, and then receives the history as before. Both the QR code and the pairing code went this way. If that new connection fails, wa connects once more itself a few seconds later, within the same minute, instead of waiting the minute out. Every other command's wait for its connection uses the same loop now, so it no longer spins while the old connection is being closed. When the phone has pushed nothing after 20 seconds,session startsays once that it is still waiting, so the pause does not look like a hang.- A pushed community no longer loses its mark. The store replaced a chat's
provider_metadatawhole on every push, so a later push that did not say a group was a community tookcommunity: trueoff it. A key a push leaves out now keeps its stored value, and so does a group'sparent_chat_id(cli-core 0.346.0). Engine protocol 35. inboxandreviewcheck--json=<fields>against the message summary's fields, and refuse a name it does not have instead of printing empty items.--briefand--fullno longer mention mail (cli-core 0.345.0).session startandsetuplink a new profile. They failed before showing a code with "cannot open the lock beside the device store": nothing made the profile's own folder before the engine opened its lock there. The engine now makes it, mode 700, before it takes the lock.- One pushed event that
wacannot handle no longer stops the ones after it.serveandwatchhandle what WhatsApp pushes one event at a time, in order. A history push or a live message whose handling failed used to stop the queue, so no later history reached the local store untilwarestarted. Now that event is skipped, a warning on stderr names its kind and error code (never its content), the next event goes on, and whenwastops listening it says how many were skipped if there was more than one. A failure that means the engine or the connection is gone still ends the listening, as before. - A message that arrives while no
serveruns is no longer lost. WhatsApp keeps messages for a linked device while it is offline and hands each one over once, to the next connection. When that connection was a one-shot command (messages send,doctor --online, an MCP session),wareceived the messages and saved none of them. Now every connection saves what arrives, and the command waits for those saves before it exits (cli-core 0.343.0). A history push that arrives during such a command is saved too (above). - A file your phone uploaded again stays downloadable, once its new copy downloaded. On the phones tried so far
it does not (see the known limitation under
messages download).wakeeps the new copy's address in the store, so a latermessages downloadof it no longer asks the phone again, andmessages forwardsends a file that opens instead of the expired one. The same holds forattachments extract --download,messages transcribe --localand the picture index (images embed). - A history push after linking again keeps that address. The phone pushes each file with the address it had before, and the store used to save it over the new one, so the next download asked the phone again. A message the store does not hold yet still takes the pushed address.
- A download your phone could not give back says why, in its
reason, not only in its words. A file WhatsApp no longer keeps fails, per file inbatch.failures, withmedia.not-on-phone(the phone has it no more),media.phone-cannot-decrypt(the phone could not read the request),media.phone-failedormedia.phone-answer-unreadable(the phone answered an error),media.phone-timeout(no answer in 20 s),media.retry-failed(the new copy did not download) orengine.stopped; branch on those, never on the message. A download the phone's new upload served writes one line,media.reuploaded, to the engine's log whenWA_ENGINE_LOGis set: the download's answer has no field for it. - A new copy that did not download says why. The failure's message ends with the cause in fixed words: the HTTP
status WhatsApp answered, a checksum or key that did not match, a file too short, a network error or the time
running out, and whether the phone gave the expired path back. With
WA_ENGINE_LOGset, amedia.refetch-failedline adds the first download's cause and the phone's result. A phone that answered an error but still gave a path is tried as before; when that path does not download either, the reason is the phone's answer (media.phone-failed,media.not-on-phoneormedia.phone-cannot-decrypt), notmedia.retry-failed, which now means the phone said it uploaded the file. - A new copy that no longer matches the message's checksums is checked against the message's key before it is
refused. If the phone encrypted the old file again under the same key,
waaccepts the copy when its MAC matches that key, which proves it was encrypted by someone holding the key, and stores the new checksums with the new address; a copy whose MAC does not match is still refused withmedia.retry-failed("the file did not match its key"). On the phones tried so far, every new copy was encrypted under a different key and was refused this way (see the known limitation undermessages download). - Node 22 no longer prints
ExperimentalWarning: SQLite is an experimental featureon every command that opens the store. Other warnings still print. wa mcp --helpand the root help suggestclaude mcp add wa -- wa <profile> mcp, where they suggested a command without a profile that always failed.- The commands page labels
conversations embed,conversations build,search conversationsand the model downloads as sending text to, or reading from, an outside service, not as changes in WhatsApp. - A chat muted for good on the phone is stored as muted, so
inboxleaves it out. The history the phone sends marks such a mute with an end no clock can read (WhatsApp writes -1 into an unsigned field), andwatook that for a mute that had already ended. Any end past the year 9999 now reads as muted with no end, as whatsmeow reads it in app state. How the phone writes it in history is not yet checked live. - A lock another process holds no longer stalls
serve(cli-core 0.340.0): the send journal, the request pace andrecipients addused to wait for it by blocking, so every relayed command and the connection's keep-alives stopped for up to 5 seconds. They now wait without blocking; thetimeout(exit 9) is unchanged. setupwithout a terminal no longer waits 5 minutes on a dead connection after the history push (cli-core 0.340.0): the steps after it get the usual 30 seconds. At a terminal they stay unbounded.- The MCP
messages listtool no longer offerstopic:(cli-core 0.340.0): WhatsApp has no forum topics, and atopic:term is refused as an unknown field, as on the command line. chats admins addhas a summary that fits in 80 characters (cli-core 0.340.0): "make a member an admin, with every right";--helpstill names the seven rights.- A late answer from the phone no longer ends the next
messages syncpage early: each request for older messages goes under its own id, and an answer that names an earlier request is saved like any history the phone sends instead of being taken for the current page. An empty late answer used to read as the chat's start. This holds if the phone names the request by that id, which is not yet checked live; otherwise answers are matched by their chat, as before. - A page the phone sends in pieces is filled: when the phone sends fewer messages than asked,
waasks again from the oldest one it sent, one second apart, at most three asks and 25 seconds per page; what arrived before a timeout is kept. - A rate limit or a ban that ends
watchorserve's feed holds the next command's writes as a one-shot command meeting it would. images embedruns insideservewhenserveholds the connection (cli-core 0.337.0): it downloads pictures through WhatsApp, so it is a read from WhatsApp, and its MCP tool has noofflineinput.- Model downloads no longer go through a running
serve(cli-core 0.336.0):models text download,models audio downloadandimages models downloadfetch from a model host, so they run in the calling process. wa chats list --json | headends quietly (cli-core 0.328.0): through a runningservethe client kept writing into the closed pipe and printed a stack trace; it now exits 0.- The phone-number and agent prompts of
session startandsetupgo to the run's own stderr, so a test or an embedding program that captures the run sees them. polls voteandpolls votersno longer mention anonymous polls in their help and their agent tools (cli-core 0.328.0): WhatsApp has none.--json=item.<field>keeps that field (cli-base 0.44.0):--json=item.profileprinteditem: {}.- A query with a
/that opens a regular expression and never closes it says to quote the value (cli-base 0.44.0):text/plainnow hints"text/plain". The error code and reason are unchanged.
Added
messages list <old id> --offlinefinds the chat that id moved to, and so does every other command that names a chat by the phone-number id it had before WhatsApp gave the person a LID. The store now keeps each chat's history: its id, title, membership, scope, search visibility and parent, one row per change. A read by an old id follows it within the account, also for a chat the account left when the command reads its kept history (cli-core 0.368.0). Only a move the store recorded is followed: oneserveorwatchmakes from now on, or one they repeat. The engine repeats the moves of the last 30 days at each start, so those are recorded at the firstserveorwatchstart after the upgrade. An older move, and a number that was never a stored chat, still read as typed. The store moves to version 25 the first time this version opens it. That cannot be undone, so take a copy first (wa <profile> store backup <file>).wa0.1.0 and older tg and max builds still open the upgraded store and write to it.wa <profile> store clear --leftlists the chats it would delete, by id, title and message count, when it refuses without--allow-dangerous, and after deleting lists what it deleted;--dry-runlists them all and deletes nothing (cli-core 0.353.0).wa <profile> chats events <group>lists who joined, left, was added, removed, promoted or demoted, and each new title, from the local store, without connecting.wakeeps the group service messages the phone pushes with the history and, underwa serveandwa watch, each change as it happens; a change seen both ways is kept once.wa watch --eventsprints them aschat_eventlines, andstats chats showcounts the joins and leaves inmembers, where it used to leave them out. Changes from before the link that the phone did not push are not there. Engine protocol 33.wa <profile> messages send <chat> --sticker-file <file.webp>sends a.webpas a WhatsApp sticker, alone, with no text; agents sendsticker_filethroughwa_write(command: "messages send"). It is a message like any other: the profile's permissions, the send journal and--operation-idapply, and so does the file guard (--allow-any-filefor a file inwa's own folders). A file that is not a WebP, or is larger than 500 KiB, is refused before connecting. A sticker to a channel is refused for now.wa <profile> chats update <group> --remove-phototakes a group's photo away; agents sendremove_photothroughwa_write(command: "chats update"). The members see it. It goes through the profile's permissions underchats.update, as--photodoes, and is refused beside--photobefore connecting. The photo goes first: if a title, description or setting given with it then fails, the error says the photo was removed.chats update --photoandaccount update --photosay they take a.jpgor.jpegfile, and refuse a file named otherwise, as they already refused one whose first bytes are not a JPEG's, before connecting. The engine still checks the photo is a square of 190 to 720 pixels a side.wa <profile> conversations rebuildbuilds, then embeds on this machine, the chats that changed and the group chats never built, or only--chat, bounded by--max-chatsand--max-chunks. It sends nothing anywhere.setupandsession startwarn about bans before they show the QR code or the pairing code. The warning says thatwais an unofficial client, that WhatsApp may ban an account for using one or for bulk or unsolicited messages, whatwadoes to keep the risk low, and what not to do. It goes to stderr once per link, in every output mode and with--quiettoo, since it belongs to the code the person is about to scan; stdout and the JSON answer are unchanged. In machine mode, a link that fails after this point now has the warning on stderr before the error JSON, so read the error from the last line. A device that is linked already, and every later command, prints nothing new. The README, the installation and linking pages and the agent skill carry the same warning. The MCP page has its own: by default an agent may send up to 30 messages an hour without asking, and how to lower that.wa <profile> chats muteandchats unmute: stop a chat's notifications for good or--untila time (2h,7d,2026-09-25T09:00), and hear it again; agents run both throughwa_write. It is the account's own setting, which WhatsApp syncs to the other devices; nobody in the chat is told (not yet checked live on the phone). It is a write and goes through the profile's permissions, underchats.muteandchats.unmute; a setting forchats.mutealone still covers both. A device linked minutes ago may not have WhatsApp's settings keys yet: the command then answers exit 12 with reasonsettings.keys-missingand says to keepserverunning a few minutes. A channel is muted on the phone.wa <profile> chats archiveandunarchive,chats pinandunpin, andchats mark-unread: move a chat into WhatsApp's archive and back, keep it at the top of the chat list, or mark it unread, on the account's devices; nobody in the chat is told. Agents run them too, throughwa_write(command: "chats archive"and the rest).chats pinpins the chat, not a message (messages pindoes that). Archiving a chat also unpins it. Archive, unarchive and mark-unread name the chat's newest stored message, aschats deletedoes, and refuse a chat the store holds nothing of (exit 6); pin needs no message. WhatsApp's apps keep three pinned chats;wadoes not count them. A mark-unread sends no receipt;chats mark-readtakes the mark off (see Changed). Channels are refused. Each answerssettings.keys-missingaschats mutedoes, and has its own permission key (chats.archive,chats.unarchive,chats.pin,chats.unpin,chats.mark-unread), allowed by default. Not yet checked live on the phone.wa <profile> chats deleteandchats clear: delete a chat, or every message in it, from this account's devices; the others in it keep theirs. WhatsApp deletes up to a message it is told of, sowanames the chat's newest message in the local store, the owner's own or not, and refuses a chat the store holds nothing of (exit 6) rather than guess. A message that arrived after the store last heard should stay.chats cleardeletes starred messages too; neither asks WhatsApp to delete the photos and files the phone saved from the chat. Both ask first at the default permission level (chats.delete,chats.clear;--allow-dangerousskips the question), are not offered over MCP, and answersettings.keys-missingaschats mutedoes. The local store keeps the chat's messages. Not yet checked live on the phone.wa <profile> messages starandmessages unstar: star one message so it is easy to find again, or take the star off. It is the account's own mark, which WhatsApp syncs to the other devices; nobody in the chat is told. The message must be in the local store, since WhatsApp names it by its sender; one the store lacks is exit 6 and nothing is sent. A channel's post is starred on the phone. It goes through the profile's permissions (messages.star,messages.unstar), an agent can do it over MCP, and it answerssettings.keys-missingaschats mutedoes. The local store keeps a star set withwa, somessages list <chat> starred:trueandsearch messages is:starredfind it. Whileserveorwatchruns, a star set or taken off on the phone reaches the store too, andwatch --events --jsonlprints it as{ "event": "star", "chatId", "chatTitle", "messageId", "starred" }; the messages the phone pushes after linking keep their stars. The store moves to version 24 for this the first time this version opens it;wa0.1.0 still opens it. Not yet checked live on the phone.wa <profile> messages pinandmessages unpin: a message in the local store is pinned for the whole chat; a message the store lacks isnot_found. WhatsApp tells the chat's members about every pin, so each one notifies and counts toward the hourly limit;messages pintakes no--notify, and an agent'swa_writecall nonotify.--expire-time 24h,7dor30d(MCPexpire_time) says how long it lasts; without it the pin lasts 7 days, as the help says; any other length isvalidation_error, over MCP before it connects.wa <profile> account privacy showandaccount privacy set: who sees last seen (--last-seen), the profile photo (--profile-photo) and the about text (--about-text), whether online and last seen are hidden (--hide-online), whether read receipts go (--read-receipts; off also hides others' receipts from the owner, as on the phone), who may add the account to groups (--chat-invites) and whose calls ring (--calls).showasks WhatsApp each time, and reports a setting kept on the phone as "my contacts except…" ascontacts_except, whichsetrefuses; for groups that value is left out.setchanges only the settings named and answers them all; it is a write underpermissions.account.privacy.set. WhatsApp has no setting for who finds the account by its number or who sees the number, so--find-by-phoneand--phone-numberare not offered. Refused before anything is sent:--chat-invites nobody(WhatsApp is reported to refuse it),--calls nobody, and--hide-onlinewith--last-seen, which both set last seen.--calls contactssends the value meant as the phone's "silence unknown callers" (not yet checked on a real account).--hide-online offshows last seen to contacts.setsends one request per setting: if WhatsApp fails one, those sent before it stay changed, and the error says so; a change cut by a dropped connection answersoutcome_unknown(exit 14), since it may have gone through.wa <profile> account updatesets the owner's WhatsApp name, about line and profile photo:--first-namesets the name people see, with--last-namejoined to it after a space (WhatsApp keeps one name, so a last name alone is refused), and--descriptionthe "about" line. The name is a setting WhatsApp syncs to the other devices; others probably see it only once the phone next comes online, since a device tells WhatsApp its name when it says it is online andwanever does (not checked live).--photosets the profile photo (not checked live):watakes a square JPEG of 190 to 720 pixels a side, as for a group's photo, checked before anything changes. The answer is the account with the new name. A device without WhatsApp's settings keys yet answerssettings.keys-missing, aschats mutedoes; the name goes first, then the about, then the photo, and a step WhatsApp refuses after another changed says which did. The permission key isaccount.update.wa <profile> chats update <group> --photo <file>sets the group's photo. WhatsApp takes only a square JPEG of 190 to 720 pixels a side, andwadoes not convert one: anything else is refused withvalidation_errorbefore WhatsApp is asked, a PNG or WebP by its bytes whatever its name. The photo is set before the title, description or settings given with it, so a refused photo leaves the group unchanged, and a--titleover 25 characters is refused before the photo is set. If the rest of the change then fails, the error begins "the photo was set", keeping its code. It runs under the same permission and hourly limit as the rest ofchats update.--remove-phototakes it away (above).- A group says which WhatsApp community it is in. A group linked to a community answers with
parent_chat_id, the community's id; the community itself is a group withprovider_metadata.community: true. They come with the group as WhatsApp gives it: the answers ofchats update,chats createandchats join.waalso reads them from the history the phone pushes, so the local store keeps them andchats listandchats showshow them, and follows WhatsApp's notices that a group joined or left a community (see Fixed). Listing a community's groups, and linking or unlinking one, are not built. - A photo and a video in one send go as an album (
messages send --photo … --file clip.mp4): one message that announces the album, then each file tied to it. The text is the photo's caption and the answer is the photo's message, under the send id. A repeat with the same--operation-idafter an unknown outcome sends the same message ids, which WhatsApp takes as the same messages, as it does for a single send. A document beside a photo is still refused. Checked on a phone: it shows as one album. - A sent photo carries its preview: the chat shows a small blurred picture before the download. Not yet for a video or a WEBP photo.
wa <profile> metadata getandmetadata refreshread a group's title and description into the store fortags auto; the group is not changed, and agents get both. A channel in the list answers with an error of its own.tasks agendalists what others owe you (cli-core 0.328.0):assignedin JSON and an "assigned — others owe you" section hold the open tasks someone else does, oldest first, each with itsassignees.wa commands --jsonhas two new keys (cli-base 0.44.0):runnable: trueon a group that runs on its own, andoffline: falseon a command that cannot answer offline. A reader that rejects unknown keys must accept them.wafollows and reads WhatsApp channels, the one-way chats whose ids end in@newsletter:chats join https://whatsapp.com/channel/<code>follows one andchats leave <channel>unfollows it, both through the profile's permissions, and the channel's admins see that you followed. The channels you follow reachchats listaskind: "channel"whenserveorwatchconnects.messages sync <channel>reads its posts into the local store from WhatsApp's servers, not from your phone: newest first, 50 at a time. Each post keeps its message id, and WhatsApp's number for it is inprovider_metadata.serverId. Every post's sender is the channel, so a post you made showsoutgoing: false. See channels.waposts to a WhatsApp channel you run:messages send <channel> <text>asks WhatsApp for your role in the channel first and refuses anyone but its owner and admins withpermission_error(exit 5) before anything is sent. The channel's followers see the post at once. Not a reply; a poll, a vote, a forward, an edit or a deletion in a channel is refused before anything is sent. A refused post still passes the profile's send permissions first, so underaskit asks before it is refused. Not yet checked live.waposts a photo, a video, a voice note or a file to a WhatsApp channel you run:messages send <channel> --photo/--file/--voice <path>, the text as the caption. The role is asked first, as for a text post, so a channel you only follow is refused (exit 5) before anything is uploaded. A channel's files are not encrypted:wauploads them as WhatsApp's apps do for channels, and the post carries no file key. One file per post; an album to a channel is refused (exit 2). A forward of a channel's file into a chat is refused too (exit 2,media.channel-file-forward), since it would arrive as a file that does not open. Not yet checked live: that needs a channel you own or administer.
0.1.0 — 17.10.2026
Changed — may break callers
- The engine dying during a write answers
outcome_unknown(WC-LIFE-9): exit 14,retry.safe: false, theoperation_idkept, reasonengine.stopped-mid-write. It answeredprovider_unavailable(exit 12), which invited a retry and a double send. A read cut the same way still answersprovider_unavailable. - Device and account states carry a
reason(WC-OUT-15), so an agent branches on it instead of the message:device.in-use(anotherwaprocess holds the login: stop it, or run the command throughserve),engine.stopped,client.outdated(updatewa) anddevice.replaced, allprovider_unavailable;device.logged-out(authentication_error);account.banned(permission_error);account.rate-limited(rate_limited). After WhatsApp ends the connection for one of these, later requests in the same process answer with it instead of connecting again. Engine protocol 10. - WhatsApp's rate limit and a ban hold the profile's sends (WC-PERM-18). After WhatsApp asks the account to slow
down, sends, edits and the other writes counted toward the send limit are refused for 15 minutes and reads go on;
after a ban, until
wa <profile> doctor --onlineconnects again. Reactions and read receipts are not held.doctor --onlinereports a ban asstanding.state: "frozen"(it saidbanned, which held nothing).wa <profile> flood clearlifts either hold. Both lengths are a default the owner may change. commands --jsonmarks more commandsmcp: false(cli-core 0.326.0, WC-MCP-2):store,server,config,mcp,models,images models,skill,runs,doctor report,upgrade,complete,commands schema,account sessions end, and the commands that change a later run's authority (recipients,chats rules). The MCP tool listwa mcpserves is unchanged.- In a group tagged
watch-tasks, any member's direct reply closes a question (cli-core 0.325.0), with the resolution "answered in the group". Before, only your answer closed it; a later message that does not reply to the question still closes nothing. - A device linked before answers without counts (WC-OUT-13).
session start --jsonleaves outreceived, andsetup --jsonleaves outhistory, when the profile was already linked: nothing was pushed, so nothing was counted. They used to say 0, which read as "the phone pushed nothing". - Refusals that need a terminal say so in
reason(WC-OUT-15).session startandsetupwithout a terminal, andsession start phonewithout one, answerreason: "input.required";--qr-filewith the phone method namesparam: "--qr-file". - No automatic replies (cli-core 0.324.0).
wano longer hasreplies, andservenever answers by reply rules, even withpermissions.replies.sendatallow: WhatsApp bans accounts that answer like a bot, andwasends nothing the owner did not ask for. A rules file left in the config folder is ignored. - Options WhatsApp has no counterpart for are no longer offered (cli-core 0.324.0):
--silentand--send-asonmessages send,messages forwardandpolls create,--at-timeonmessages send,--anonymousonpolls create, and the commandschats send-asandmessages scheduled. They were refused before; now they are unknown, still with exit 2, and the MCP tools no longer takesilent,send_as,at_timeoranonymous. - Commands WhatsApp lacks refuse before connecting (cli-core 0.324.0):
account update,account sessions listandend,chats events,chats inspect,chats folders *,contacts add,remove,renameandimport,messages pinandunpinrefuse with exit 2 before connecting to WhatsApp. watch --eventsno longer prints the votes of the phone's history push (cli-core 0.322.0). They were printed as new votes; they are old ones, and the store now saves them with the push, together with their poll and dated when each person voted, so an older one never replaces a newer live vote. A script that read history votes fromwatchreadspolls votersinstead. Engine protocol 9.- An agent never deletes for everyone (cli-core 0.321.0). Every WhatsApp delete
wamakes is for everyone, somessages deletethrough MCP is refused withpermission_error, beforewaconnects, whatever the profile's level; the owner still haswa <profile> messages delete --for-everyone. mcp --permission <key=level>only narrows (cli-core 0.321.0). An override that would give the server more than the profile —readonlytoallow, adenyto anything — stops it at startup withvalidation_errornaming the key. To let an agent do more, set the level in the profile. The browser setup no longer starts the server with--permission messages.send=allow.- Tasks open only where something is asked of you (cli-core 0.318.0). In a one-to-one chat every question you did not answer opens one, as before; in a group only a question in reply to your message and a mention of you; in a channel or the chat with yourself nothing.
tasks applyis nowtasks scan(cli-core 0.320.0); MCPtasks_scan, permission keytasks.scan.- No setting picks a profile any more (cli-core 0.319.0). A command names its profile with the first word or
WA_PROFILE. A config file that still hasdefault_profilestops a command that names no profile, with the profile to type andwa config unset default_profile;wa config set default_profileis refused. A profile calleddefaultis an ordinary name:wa default mcp confignames its serverwa-default, notwa. polls closeandpolls create --revoteare gone (cli-core 0.317.0): a WhatsApp poll never closes and every vote can change. A script that closed a poll or asked for revoting drops that call.chats admins addtakes no--can: a WhatsApp admin has every right, so it gives them all. A script that passed--candrops it.
Security
WA_ENGINE_LOGno longer writes phone numbers (WC-SECR-1): every user id (phone, device and hidden-id forms) and any other long run of digits is masked before a line reaches the file.- A release from GitHub Actions publishes the engine packages validation built and tested (WC-REL-12): it checks each against the SHA-256 that validation recorded, and builds nothing again. This first release was published from the maintainer's machine, which builds them itself.
WA_READ_ONLYalso stops linking and unlinking the device:session startandsetuprefuse to link a new device, andsession endrefuses to log out, withpermission_error.- An interrupted or timed-out login closes the engine and removes its QR image, instead of leaving both behind.
- The account name shown after linking is printed as plain text, never as terminal control codes.
- Media keys stay inside the client: internal attachment references stay out of live-event, JSON and MCP output (cli-core 0.315.0).
- Invite links must name WhatsApp's exact invite host; unrelated URL prefixes are refused before connecting.
- WhatsApp state is safe from the other messengers' uploads (cli-core 0.314.0), and a running server's read-only lock holds when a command is relayed to it.
- The Go engine builds with Go 1.26.9 and
golang.org/x/net0.60.0, clearing the affected runtime and networking advisory paths while retaining the current Go release branch.
Fixed
session endno longer runs through a runningserve: it logged outserve's own connection and then deleted the device keys under it. It now refuses withprovider_unavailable, reasondevice.in-use(exit 12), and says to stopservefirst (wa <profile> server stop, or Ctrl+C).- The command reference no longer says
defaultProfilein the config file picks a profile (WC-ACCT-1): no setting does; the first word orWA_PROFILEnames it. - Refusals name the whole command to run next:
wa <profile> chats list,wa <profile> serveandwa <profile> session start, where they said onlychats list,serveorsession start. - No
messages syncadvicewacannot follow (cli-core 0.322.0): where a command points at the store's gaps it namesmessages sync <chat>for one chat, never--all. wa upgrade's "held back" hint names the package and keeps pnpm's release-age exemption (cli-core 0.320.0).- An agent's
chats members listandchats send-asfollow their own permission keys,chats.members.listandchats.send-as, so a deny on those keys reaches the MCP agent too (cli-core 0.321.0). - A reply names the quoted message's sender and carries its text when the local store holds that message, so phones show the quote instead of "unavailable"; a reply to one the store lacks still goes, with the message's id alone.
- An edit keeps the message's own time: one that
serveorwatchhears keeps the message's time, reply and reactions in the local store; before, it took the edit's time as the message's. - Tests on Windows check file access rules instead of Unix mode bits. Group administration tests verify stranger refusals and unsupported changes before writes.
- The search and query language pages said the file fields find nothing;
has:file,filename:,mime:andcontent:work on every message stored sincewahandled files. The README and the docs start page no longer list files and group administration as coming next. - Release and monthly source checks include the Go engine, using its module toolchain. Local tests resolve the shared TypeScript config directly so they run in isolated worktrees.
Added
wa <profile> flood clear(cli-core's, as in tg and max): forgets the waits WhatsApp asked for and lifts the hold on writes. The refusal during a hold already named it;wadid not have it.session,session start,session endandsetupcarrymcp: falseincommands --json(WC-CMD-21): linking waits for a person and unlinking removes the owner's device, so neither will be offered to an agent once MCP tools are built from commands.setupandupgradelist examples there too (WC-CMD-24).- The engine packages are GPL-3.0-only:
@wirecat/whatsapp-engine-*and theengine/folder they are built from, because the engine linksgo.mau.fi/libsignal(GNU General Public License 3.0). Each package carries the GPL text asLICENSEand a README that points at the commit it was built from.@wirecat/whatsapp-clistays Apache-2.0: it runs the engine as a separate program over stdin and stdout, and its package never carries the engine. - Each engine package carries
THIRD_PARTY_NOTICES: the licence of every Go module the engine links and of Go itself, with where each module's source is. whatsmeow andgo.mau.fi/utilare under the Mozilla Public License 2.0 andgo.mau.fi/libsignalunder the GNU General Public License 3.0, all unmodified. - Tasks have assignees (cli-core 0.320.0):
tasks addandtasks plantake--assignee sender(whoever sent the message) or--assignee person:<id>;noneclears it.tasks list "assignee:any state:open"is what others owe you. wa <profile> tags add --chat <group> watch-tasksrecords every question in that group as a task, whoever it is for.- Poll votes are counted:
serveandwatchkeep each person's newest vote, from the phone's history push and as votes arrive (watch --eventsprints them), sopolls showcounts the answers and says which you chose, and the newpolls voterslists who voted for what. A poll made withpolls createis kept in the local store, so it can be shown and voted on. Engine protocol 8; cli-core 0.317.0. - Commands while
serveruns: a send, an edit, a reaction,watch,wa <profile> mcpand every other command that connects run inside a runningserve, on its one connection, instead of being refused. Permissions and the send guard still apply; a write ataskneeds--yes. On Windows they do not go throughserve. - The foundation: the shared cli-core commands over a WhatsApp adapter that reads from the local store, an engine contract, and the same linters, checks and CI as tg-cli and max-cli.
- Older history from the phone:
wa <profile> messages sync <chat>asks the phone for the 50 messages of a chat before the oldest one the local store holds, page after page, and waits up to 30 seconds for each answer (engine protocol 5; cli-core 0.316.0).--allis refused: WhatsApp has no chat list to walk. - More group settings:
chats update --only-admins-send,--only-admins-edit-infoand--disappear-time(24h, 7d or 90d, the times WhatsApp offers;offkeeps messages). A group's answer shows them insettingsinstead ofprovider_metadata. Engine protocol 7. - The WhatsApp engine:
waconnects through a Go program built on whatsmeow, started for each command that connects and stopped when it ends. One command at a time may connect a profile; a second is told which one holds it. - Installs with its engine:
npm install --global @wirecat/whatsapp-clialso installs the engine for your system, a small package of its own, for Linux, macOS and Windows on x64 and arm64.WA_ENGINEstill runs another one. A global install also installs the agent skill and, on Windows, putswaon the user's PATH, as tg does. wa <profile> session start [qr|phone]links the profile by QR code or pairing code and keeps the history the phone sends right after linking.session endunlinks the device and deletes its keys.- Changing messages:
messages edit(your own, within WhatsApp's 20 minutes),messages delete --for-everyone(your own, or another's in a group you administer),reactions addandreactions remove, andchats mark-readand--mark-read, which send a read receipt only when asked. Each needs the message in the local store, whichservekeeps filled; WhatsApp addresses a message by its sender. Deleting for you alone is not built yet. - Files: photos, videos, voice notes, audio, documents and stickers arrive as attachments, with places and contact
cards named;
messages send --file,--photoand--voicesend one file with its caption;messages downloadsaves a stored message's file;messages transcribe --localreads a voice note with a model on this machine (wa models);messages forwardsends a stored message again, marked forwarded, file included;wa imagessearches pictures by what they show. The engine protocol is now 3. - Groups and people:
chats create,join,leave,update(name, description, admin-only adds, join approval),chats link show|reset,chats members add|remove,chats admins add|remove,chats requests list|accept|decline,contacts profile,contacts block|unblock,contacts lookupandcontacts sync(the phone's address book), anddoctor --onlinenow connects to check the account. Group changes go through the send guard like a send; adding people is refused for anyone not in the address book or written to before, since WhatsApp bans accounts that add strangers. A number lookup is one per call, one every 10 seconds per profile. Engine protocol 4. - Formatting and polls:
messages send --mdandmessages edit --mdwrite Markdown as WhatsApp's own marks (bold, italic, strike, code, quotes; a link becomes its label and address; underline and spoilers become plain text).polls create(one answer or--multiple),polls showandpolls vote(--retracttoo) work on polls in the local store. Engine protocol 6. wa <profile> setupis the guided first run, as in tg: it checks this computer, links the device or reuses it, reads the account and five chats from the local store, and installs the agent skill (--agent). Agents use--qr-file.wa upgradeinstalls the newest version with the package manager that installedwaand restarts running servers; a daily line on stderr says when a newer version exists.wa doctorreports whether the engine program is found, whether the device keys exist with private file modes, and who last held the device's lock and whether that process still runs.- Documentation like tg's: pages on linking and profiles, the local store, search and its query language,
groups, people, MCP and the browser, permissions, limits, the CLI contract, diagnostics and recipes, each limited
to what WhatsApp can do today. The agent skill (
wa skill show) is rebuilt on tg's: setup without a terminal, the traps, the exit codes, and what one connection per device means for an agent. - A page on file attachments (docs/attachments.md): sending one file per message, what the
local store keeps, downloading, forwarding, voice notes as text with a model on this machine, and reading the text
of files for
content:search. The README gains tg's sections on linking and profiles, groups you run, limits and the list of pages.