Repository navigation
0.2.0
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: runrecipients 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 nextserveor
watchstart, 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
-
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 with
settings.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 messages
takesquery(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,cursoror
gemini) 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, set
permissions.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_failed
in 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 upgrade --offlineexits 2 instead of asking npm anyway. -
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 startand
setupon 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 exit5
too, 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, and
until: 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, exit
5,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 start
keeps 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 gets
validation_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", aschats 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 signed
oh/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 incontacts 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:120363000000000001@g.us
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 showand
messages listalready did. Before, these refused a left group by every name and id.chats tracking addand
store 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 startand
setupsaid 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 asmessages 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 undermessages 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-failedor
media.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 when
WA_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. WithWA_ENGINE_LOGset, amedia.refetch-failed
line 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-phoneor
media.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 a
topic: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 in
members, 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_photo
throughwa_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 for
chats.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, which
setrefuses; 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, as
chats 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_error
before 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 for
tags 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, and
offline: 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 list
askind: "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.