wa
Personal WhatsApp automation CLI + daemon, written in Go.
A hexagonal Go daemon that owns a WhatsApp Multi-Device session and a thin JSON-RPC client that talks to it — safe enough to let a language model send messages on your behalf, crash-safe enough to survive a power loss mid-migration, and paranoid enough to refuse every destructive flag you might expect.
Quickstart · Install · Manual · Architecture · Security · Contributing · Português (BR)
</div>Capability
Pattern. Persistent WhatsApp daemon over a Unix socket — one wad process holds the Multi-Device session, ratchet store, and websocket; a thin wa JSON-RPC client invokes it.
Trade-off. ~30 MB RSS per profile and a 5–8 s cold connect, in exchange for sub-second warm-call latency on every subsequent wa send. Per-call session reattachment is avoided entirely.
Use when. A shell pipeline, cron job, or Claude Code plugin needs to dispatch WhatsApp messages with predictable latency and a non-overridable safety pipeline (default-deny allowlist + rate limiter + warmup ramp + append-only audit log) sitting below every RPC path.
brew install yolo-labz/tap/wa
wad & # daemon, single instance per profile
wa pair # QR-code pairing on first run
wa allow add 5511999999999@s.whatsapp.net --actions send
wa send --to 5511999999999@s.whatsapp.net --body "hello"
Demo
A non-interactive 15-second asciinema cast covering wa --help, wa daemon status, two wa send calls (warm-call latency under 500 ms), and wa allow list is checked into the repo at docs/assets/wa-demo.cast. Replay locally:
asciinema play docs/assets/wa-demo.cast
A hosted player embed will land in a follow-up PR after the cast is uploaded to asciinema.org.
How wa compares
Closest peers in the reverse-engineered-WhatsApp ecosystem:
| Capability | wa (this repo) | whatsmeow direct | whatsapp-web-cli |
|---|---|---|---|
| Persistent daemon (sub-second warm-call) | yes | no (per-call session attach) | no (browser-driven) |
| JSON-RPC over Unix socket | yes | n/a (library, not a daemon) | no (Chrome bridge) |
| Default-deny allowlist (per-action) | yes | manual implementation | no |
| Non-overridable rate limiter (2/30/1000) | yes | manual implementation | no |
| Warmup ramp for fresh sessions | yes | manual implementation | no |
| Append-only JSON-Lines audit log | yes | manual implementation | no |
| SLSA L2 + Sigstore signed releases | yes | n/a | no |
| Dual SBOM (CycloneDX 1.6 + SPDX 2.3) | yes | n/a | no |
CGO_ENABLED=0 static binary | yes | depends on consumer | no (browser-driven) |
| Inbound prompt-injection firewall | yes | n/a | no |
For multi-tenant REST gateways see EvolutionAPI or WAHA — different shape of problem, listed in What this is NOT.
What this is
Two binaries, one repo:
wad— long-running daemon that owns the WhatsApp session, the SQLite ratchet store, and the websocket toweb.whatsapp.com. Runs undersystemd(Linux),launchd(macOS), or a NixOS module. Single-instance per profile, never as root.wa— thin JSON-RPC client that speaks towadover a unix socket. This is what shell scripts, cron jobs, and Claude Code plugins actually invoke.
It is built on go.mau.fi/whatsmeow — the library that powers mautrix-whatsapp at production scale — because it is the only reverse-engineered WhatsApp library actively maintained in 2026. There is no MCP server in this repo by design.
What this is NOT
- Not a bulk-messaging tool. The rate limiter is non-overridable and there is no
--forceflag anywhere. - Not a multi-tenant SaaS. Each
wainstall is scoped to one person, with optional multi-profile isolation for work/personal splits. - Not a Matrix bridge. Use
mautrix-whatsappif that's what you want. - Not a REST gateway. Use
EvolutionAPIorWAHAif that's what you want. - Not the official WhatsApp Cloud API. This project uses the reverse-engineered Multi-Device protocol via
whatsmeow.
Quickstart
# Install (Homebrew — macOS + Linuxbrew)
brew install yolo-labz/tap/wa
# Or via Nix (recommended for NixOS/nix-darwin users)
nix profile install github:yolo-labz/wa
# Or the checksum-verified installer (80 lines — inspect first if you like)
curl -fsSL https://raw.githubusercontent.com/yolo-labz/wa/main/install.sh | bash
# Or Docker — single distroless container (~12 MB); /data holds the session
docker compose up -d # see docker-compose.yaml; pair via `docker compose exec`
# Start the daemon (default profile)
wad &
# Pair your phone — QR code in terminal
wa pair
# Allowlist yourself (default-deny policy)
wa allow add 5511999999999@s.whatsapp.net --actions send
# Send a message
wa send --to 5511999999999@s.whatsapp.net --body "hello from wa"
# Install as a persistent system service
wad install-service --profile default
# Hand it to an AI agent — MCP over stdio, draft-gated sends by default:
# the agent PROPOSES messages into a human-review queue; nothing leaves
# until you run `wa draft approve`. Add to Claude Desktop/Code, Cursor:
# {"mcpServers": {"wa": {"command": "wa", "args": ["mcp", "serve"]}}}
wa mcp serve --help
The recipient flag is spelled --to, --jid, or --group depending on the command; --chat <jid> is accepted as a universal alias on all of them (the original flags still work).
For the full tour including multi-profile setup, shell completion, migration, and the audit log, see docs/manual.md.
Install
Homebrew (macOS + Linux)
brew install yolo-labz/tap/wa
Nix flake
# One-shot
nix run github:yolo-labz/wa -- profile list
# Install to profile
nix profile install github:yolo-labz/wa
# Dev shell (go + gopls + golangci-lint + goreleaser + sqlite + jq)
nix develop github:yolo-labz/wa
NixOS module — import the system module and enable:
{
inputs.wa.url = "github:yolo-labz/wa";
outputs = { self, nixpkgs, wa, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
wa.nixosModules.default
{ services.wa.enable = true; services.wa.profile = "default"; }
];
};
};
}
home-manager module — import for a per-user installation:
{
imports = [ wa.homeManagerModules.default ];
services.wa = {
enable = true;
profile = "default";
autoStart = true;
};
}
GoReleaser tarball + supply-chain verification
VERSION=v2.0.5 # pin to a known tag
ARCH=linux_amd64 # or darwin_arm64 / linux_arm64
BASE="https://github.com/yolo-labz/wa/releases/download/$VERSION"
# 1. Download the tarball + checksums
curl -LO "$BASE/wa_${VERSION#v}_${ARCH}.tar.gz"
curl -LO "$BASE/checksums.txt"
# 2. Verify the SHA256 (defends against accidental corruption)
sha256sum -c checksums.txt --ignore-missing
# 3. Verify the SLSA-L2 attestation against THIS tarball (not just the
# manifest) via GitHub's native attestation store. From v2.0.5 onward
# every artifact (.tar.gz / .deb / .rpm / .apk) is registered as its
# own attestation subject — earlier releases (≤ v2.0.4) only attested
# `checksums.txt` and would 404 on per-artifact verify. Requires
# `gh` CLI ≥ 2.50 (Sigstore bundle v0.3.1 support). The verify step
# transparently checks Rekor v2 transparency-log inclusion proof on
# the Sigstore bundle — no separate `rekor-cli` invocation needed.
gh attestation verify "wa_${VERSION#v}_${ARCH}.tar.gz" --repo yolo-labz/wa
# 4. Install
tar xzf "wa_${VERSION#v}_${ARCH}.tar.gz"
install -m 0755 wa wad ~/.local/bin/
The gh attestation verify step proves that this exact tarball was produced by the yolo-labz/wa GoReleaser job on the exact commit SHA the tag points at. Every release ships:
wa_<version>_<os>_<arch>.tar.gz— the platform tarballchecksums.txt— SHA256 of every artifactchecksums.txt.sigstore.json— Cosign-signed Sigstore bundle forchecksums.txt(legacy/optional verify path; not needed when usinggh attestation verifyagainst the artifact above)sbom.cdx.json— CycloneDX 1.6 SBOM (full repo, syft)sbom.spdx.json— SPDX 2.3 SBOM (full repo, syft)sbom.gomod.{wa,wad}.cdx.json— Go-native CycloneDX SBOMs (cyclonedx-gomod, per binary, with stdlib + license info)openvex.json— OpenVEX 0.2 statement file. VEX-aware scanners (Trivy, Grype) consume this to filter "is this CVE actually exploitable inwa?" — most transitive CVEs arenot_affectedbecause thedepguard-enforced port boundary keeps them off the runtime path. Statements grow as findings emerge; current baseline is empty pending per-CVE triage.CHANGELOG.md— git-cliff generated changelog
darwin users — unsigned release: releases cut without an Apple Developer account ship the darwin-arm64 binary unsigned and un-notarized. Gatekeeper will quarantine it on first launch. Strip the quarantine flag before running:
xattr -cr ~/.local/bin/wa ~/.local/bin/wadOr install via the Homebrew tap (
brew install yolo-labz/tap/wa) — brew rebuilds from source locally and bypasses the quarantine entirely. Signed + notarized darwin binaries return on the next GA tag cut afterAPPLE_DEVELOPER_ID_APPLICATION_{CERT,KEY}secrets are populated.
go install
The module path includes the /v2 suffix per Go's semantic import versioning rule for v2+ releases.
# Pin to a tag (recommended — reproducible builds need a known revision):
go install github.com/yolo-labz/wa/v2/cmd/wa@v2.0.13
go install github.com/yolo-labz/wa/v2/cmd/wad@v2.0.13
# Or follow main:
go install github.com/yolo-labz/wa/v2/cmd/wa@latest
go install github.com/yolo-labz/wa/v2/cmd/wad@latest
go install on a tag does NOT inherit the GoReleaser build flags (-trimpath, -buildvcs=true, ldflag-stamped version). The resulting binary will run, but wa --version reports (devel). Use the GoReleaser tarball or the Homebrew tap if version-stamped, reproducible binaries matter to you.
Multi-profile
Run two WhatsApp accounts side-by-side — personal and work, or one per client — with full process isolation. Each profile has its own session.db, allowlist.toml, audit.log, rate limiter, unix socket, and warmup timestamp.
wa profile create work
wad --profile work &
wa --profile work pair
wa --profile work status
wa profile list
# PROFILE ACTIVE STATUS JID LAST_SEEN
# default * connected 5511999999999@s.whatsapp.net 2026-04-11T17:00:00Z
# work connected 5511888888888@s.whatsapp.net 2026-04-11T17:00:00Z
Profile selection precedence (highest wins):
--profile <name>flagWA_PROFILEenv var (empty = unset)$XDG_CONFIG_HOME/wa/active-profilepointer- Singleton (if exactly one profile exists)
- Literal
default
See docs/manual.md §4.
Safety
wa is safe enough to let a language model invoke it on your behalf. The safety pipeline is non-overridable and lives inside the daemon, below every RPC path:
- Allowlist, default-deny. Per-action (
read/send/group.add/group.create). Hot-reloaded on SIGHUP. Mutated viawa allow add/remove. - Rate limiter. 2/sec, 30/min, 1000/day per profile. No
--forceflag. Ever. - Warmup ramp. Fresh sessions run at 25 % caps for days 0–7, 50 % for days 8–14, 100 % after. Timestamp sourced from the session store so daemon restarts don't reset the clock.
- Audit log. Append-only JSON Lines at
$XDG_STATE_HOME/wa/<profile>/audit.log. Every send + every allowlist decision + every migration is recorded. Never auto-rotated — back it up yourself. - Inbound prompt-injection firewall. Inbound message bodies are wrapped in
<channel source="wa" ...>…</channel>tags before reaching Claude Code so the model can structurally distinguish "the user typed this in the terminal" from "an unknown contact sent this". - Crash-safe migration. 007→008 migration uses a write-ahead marker + single
os.Renamepivot + fsync barriers. Proven by a subprocessSIGKILLinjection test that kills the process between every step and asserts zero data loss on recovery. - Socket hardening. Unix socket + sibling lockfile opened with
O_NOFOLLOW(CVE-2025-68146), parent directory verified mode0700+ euid-owned,SO_PEERCREDcheck on every accept, umask-narrowed bind.
Full threat model: SECURITY.md.
Architecture
┌──────────────────────────┐
│ cmd/wa │ thin JSON-RPC client
│ (cobra, no business │ (stateless, re-invokable,
│ logic, no state) │ what scripts actually call)
└─────────────┬────────────┘
│ JSON-RPC 2.0 over
│ $XDG_RUNTIME_DIR/wa/<profile>.sock
▼
┌──────────────────────────────────────────────────────────────┐
│ cmd/wad │ composition root
│ │ (one process per profile)
│ ┌─────────────────────────┐ ┌────────────────────────┐ │
│ │ internal/app/dispatcher │◄──►│ Safety pipeline: │ │
│ │ (use cases, port-only) │ │ allowlist + rate lim │ │
│ │ │ │ + warmup + audit │ │
│ └───────────┬─────────────┘ └────────────────────────┘ │
│ │ │
│ ▼ 9 port interfaces │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ internal/adapters/secondary │ │
│ │ whatsmeow | sqlitestore | sqlitehistory | memory │ │
│ │ slogaudit | … │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│
│ Signal Protocol ratchets +
│ WhatsApp Multi-Device websocket
▼
web.whatsapp.com
- Hexagonal core (
internal/domain/,internal/app/) depends only on the 9 port interfaces ininternal/app/ports.go. Not one import ofgo.mau.fi/whatsmeowoutside the adapters, enforced by agolangci-lint depguardrule that is CI-blocking. - Port-boundary fakes: every secondary adapter ships an in-memory twin for tests. The contract test suite (
internal/app/porttest/) runs against any adapter. No test reaches the real websocket. - One daemon per profile. Feature 008 bounds the blast radius: a crash in one profile's daemon never touches another. Trade-off: ~30 MB RSS per profile.
- No CGO. Ever.
modernc.org/sqliteis the only SQLite path. Enforced in the Nix flake (env.CGO_ENABLED = "0"), in GoReleaser, and in thego.modtoolchain flags.
For the full design and reliability principles, see docs/manual.md.
Command reference (at a glance)
Client (wa):
| Command | Purpose |
|---|---|
wa pair | Scan a QR or use --phone for phone-code pairing |
wa status | Non-blocking connection state |
wa send --to <jid> --body <text> | Send a text message (allowlist + rate limiter apply) |
wa sendMedia --to <jid> --path <file> | Send an image/video/audio/document (over --remote, the client-local file is auto-uploaded then sent by sha256) |
wa reply --to <jid> --quoted-id <id> --body <text> | Send a quoted reply that threads under a message |
wa markRead --chat <jid> --messageId <id> | Mark a message as read |
wa react --chat <jid> --messageId <id> --emoji 👍 | Add/remove a reaction |
wa msg revoke|edit|forward|star|disappearing | Moderate an already-sent message |
wa poll vote --chat <jid> --poll-id <id> --option <n> | Vote on a poll (--option repeatable for multi-select) |
wa presence composing|recording start|stop --chat <jid> | Send typing / recording indicators |
wa groups | List joined groups |
wa group create|leave|add|remove|promote|demote|edit|invite | Administer a group (admin actions) |
wa chat list / wa chat last-active | List chats, most-recently-active first (read-only) |
wa chat archive|pin|mute|mark-unread --chat <jid> | Change chat-level state |
wa history --chat <jid> [--before <id>] | Show one chat's history (paginated) |
wa messages list --chat <jid> --media-type audio | Filter messages by chat, media kind, direction, time window |
wa search --query <fts5> | Full-text (FTS5) search across all messages |
wa thread get --chat <jid> [--cursor <c>] | Fetch a cursor-paginated message window |
wa export --chat <jid> [--since <ts>] | Export a chat oldest-first as NDJSON |
wa contacts list / wa contacts search --query <q> | List or trigram-search the local contact directory |
wa contact block|unblock|blocklist|lid|pn | Server-side blocklist + PN↔LID resolution |
wa privacy get [--key <k>] | Read the live account privacy settings |
wa privacy set --key <k> --value <v> | Change one privacy setting (e.g. lastSeen → contacts) |
wa media list --chat <jid> | List media with cache status (sha256, size, duration) |
wa media resolve|download|fetch | Resolve / lazy-fetch content-addressed media |
wa media gc --dry-run | Preview GC candidates as NDJSON + a reclaimable-bytes summary |
wa --remote <url> push <file> | Upload a client-local file to a remote daemon's media store; prints its sha256 for reuse with sendMedia --sha256 |
wa schedule send|list|cancel|update | Schedule future sends (pending → fired|cancelled|failed) |
wa draft list|get|approve|reject | Human-review draft queue |
wa labels list|create|delete|assign|unassign | WhatsApp Business labels (behind labels flag) |
wa session logout-all | Unlink every device from the account |
wa allow add <jid> --actions send,read | Grant actions |
wa allow remove <jid> | Revoke all actions |
wa allow list | Dump the allowlist |
wa wait --events message --timeout 30s | Block until an event arrives |
wa subscribe --events <types> [--since <seq>] | Stream filtered events as NDJSON (cursor-resumable) |
wa stream [--chat <jid>] | Live-tail incoming messages as NDJSON (wraps wa subscribe --events message) |
wa sync force [--chat <jid>] [--count N] | Force an immediate history pull when the DB lags the phone |
wa sync status | Show the on-demand sync engine state (in-flight pulls, queue depth) |
wa embeddings status / wa embeddings purge --yes | Inspect / drop the vector index (behind embeddings flag) |
wa health | Non-blocking liveness probe (paired/connected/last-event) |
wa doctor | Run 11 self-diagnostic checks against the local install |
wa debug pprof [cpu|heap|goroutine|block|mutex] | Capture a runtime profile from wad |
wa audit verify [--path <log>] [--key <keyfile>] | Verify the audit-log HMAC chain |
wa config features | Show resolved feature flags (embeddings, scheduled_sends, labels) |
wa profile list/use/create/rm/show | Multi-profile lifecycle |
wa migrate [--dry-run|--rollback] | Explicit 007→008 migration |
wa panic | Unlink device + wipe local session |
wa version | Version, commit, build date |
wa upgrade | Print the upgrade command for your install method |
wa completion bash|zsh|fish|powershell | Shell completion script |
Daemon (wad):
| Command | Purpose |
|---|---|
wad [--profile <name>] [--log-level <lvl>] | Run the daemon in the foreground |
wad install-service --profile <name> | Install systemd/launchd unit for a profile |
wad uninstall-service --profile <name> | Remove only the specified profile's unit |
wad migrate [--dry-run|--rollback] | Internal target for wa migrate |
Full flag reference: docs/manual.md §6.
Inspecting the daemon (when the CLI "lies")
The daemon owns the SQLite truth; the CLI is a thin client over it. When a command's output looks wrong — a chat you expect is missing, a message count seems stale — work down this ladder. The first rung resolves almost every case without touching the database.
1. First-class discovery commands (the supported path)
These read straight from the daemon and need no SQL. They replace the old
docker cp messages.db + hand-rolled SELECT workaround:
| Question | Command |
|---|---|
| Which chats exist, most-recent first? | wa chat list (or wa chat last-active) |
| What's in one chat, filtered? | wa messages list --chat <jid> --media-type audio --since 2026-01-01T00:00:00Z |
| Who's in the contact directory? | wa contacts list / wa contacts search --query <name> |
| What media is cached on disk? | wa media list --chat <jid> (SIZE / CACHED / SHA256 columns) |
| What would GC reclaim? | wa media gc --dry-run (NDJSON candidates + reclaimable bytes on stderr) |
| Messages on the phone but missing here? | wa sync force (global) or wa sync force --chat <jid> (blocks until that chat's pull lands), then re-run the query |
| Is a sync in flight right now? | wa sync status (syncing, in-flight force pulls, worker queue depth) |
| Watch new messages arrive live? | wa stream (or wa stream --chat <jid>) |
Add --json to any of them for NDJSON you can pipe to jq. Against a Dokku
deploy, run the same commands inside the container —
dokku enter <app> -- /usr/local/bin/wa chat list — or over an
SSH-forwarded socket with scripts/wa-remote chat list.
2. wa doctor
If the discovery commands themselves look impossible (e.g. zero chats on a
paired account), run wa doctor. It runs 11 checks — socket perms, pairing
state, on-disk layout version, audit-log size — and prints a hint: line for
every WARN/FAIL. A schema_version WARN means the on-disk layout drifted
from domain.LayoutSchemaVersion; run wa migrate to forward-migrate.
3. Raw SQLite (last resort)
The production image is gcr.io/distroless/static-debian12:nonroot — no
shell, no sqlite3, no ls/find. You therefore cannot
dokku enter <app> -- sqlite3 …, and there is intentionally no wa-debug
sidecar image (it would widen the attack surface the distroless base exists
to shrink). Instead, query a WAL-safe snapshot from the Dokku host, which
has its own sqlite3:
# messages.db lives under XDG_DATA_HOME → on the host storage mount:
DB=/var/lib/dokku/data/storage/<app>/data/wa/<profile>/messages.db # <profile> defaults to "default"
# Snapshot via the online-backup API (WAL-safe; never `cp` a live WAL DB):
ssh dokku.example.com sudo -u dokku-65532 \
sqlite3 "$DB" ".backup '/tmp/messages-snapshot.db'"
# Inspect the snapshot, not the live file:
ssh dokku.example.com sqlite3 /tmp/messages-snapshot.db \
"SELECT chat_jid, push_name, COUNT(*) AS msgs, MAX(ts) AS last
FROM messages
WHERE chat_jid LIKE '%@s.whatsapp.net'
GROUP BY chat_jid ORDER BY last DESC LIMIT 20;"
On-disk layout (container path → Dokku host path under
/var/lib/dokku/data/storage/<app>):
| File | Container path | Purpose |
|---|---|---|
messages.db | /data/data/wa/<profile>/messages.db | History + FTS5 |
session.db | /data/data/wa/<profile>/session.db | Signal session (never copy live) |
contacts.db | /data/data/wa/<profile>/contacts.db | Contact mirror |
| media blobs | /data/cache/wa/media/sha256/ | Content-addressed cache |
.schema-version | /data/config/wa/.schema-version | On-disk layout version |
Copying a live WAL-mode DB with cp can produce a corrupt file — always go
through .backup. See the Backups section of
docs/deploy/dokku.md.
Development
# Clone
git clone git@github.com:yolo-labz/wa.git
cd wa
# Devshell (Nix users)
nix develop
# Or use your own Go toolchain (1.26+, matching go.mod)
go version
# Build
go build ./cmd/wa ./cmd/wad
# Test (race detector on by default; ~20s wall clock)
go test -race ./...
# Lint
golangci-lint run
# Format
gofumpt -w .
# Nix build + smoke test
nix build .#default && ./result/bin/wa version
# Snapshot release (local only, no publish)
goreleaser release --snapshot --clean --skip=publish
Workflow: every change lands via PR against main, commit subjects follow Conventional Commits. See CONTRIBUTING.md.
CI/CD runs on a self-hosted GitHub Actions runner pool (label set [self-hosted, dokku], with one runner — wa-sonar-runner — additionally labelled sonar). Required-check jobs: detect, lint (golangci-lint), test (race + shuffle + coverage), sonar (SonarQube scan, consumes the cover.out artifact), nix (nix flake check + nix build .#default + smoke), commitlint (PR title), Reproducibility (two-build byte-identity), OSV-Scanner (vuln DB + Go call-graph reachability via internal govulncheck), gitleaks (secret scan), CodeQL (Go + actions). Hosted runners are reserved for OpenSSF Scorecard (needs fresh image guarantees). Release workflow triggers on v* tags and publishes GoReleaser tarballs + dual SBOMs (CycloneDX 1.6 + SPDX 2.3 + Go-native per-binary) + Cosign Sigstore bundles + GitHub-native attestations to GitHub Releases. Apple notarization and Homebrew tap publication graceful-degrade when their secrets are absent.
Project status
| Version | Highlights |
|---|---|
| v2.0.2 | Hot-fix the system.hello handshake regression (issue #41) so every CLI command works against a v2 daemon. |
| v2.0.1 | Release-pipeline hot-fixes: cyclonedx-gomod path, syft CycloneDX 1.6 cap, gomod proxy off, Apple GA gate post-rc7 amendment. Broken: CLI handshake — superseded by v2.0.2. |
| v2.0.0 | Parity hardening — 7 new ports + 20 P0 methods + idempotency + chat-state + blocker + privacy + profile editor + group admin + poll manager. OTel runtime metrics, fuzz, doctor. JSON-RPC frozen at protoVersion: 2. |
| v1.2.x | Agent-experience tier-3 release — embeddings sidecar, scheduled drafts, contact search FTS5 trigram, observability ring buffer. |
| v1.0.x – v1.1.x | First production releases — supply-chain attestations, OSV-Scanner V2, OpenSSF Scorecard, Cosign signing, dual SBOMs. |
| v0.x | Pre-1.0 — multi-profile, crash-safe migration, hardened systemd + launchd, all benchmarked SCs pass with 7.6×–870× headroom. |
Live backlog (deferred until a profile pin or external action exists): homebrew_casks: migration (replaces brews:, but Linuxbrew loses formula path on darwin-only casks — needs apt/rpm via nFPM as a Linux substitute first), LID/BSUID dual-keying in domain.JID (Meta June 2026 username rollout), default.pgo PGO profile (capture pipeline + commit), actions/attest migration off the attest-build-provenance@v4 wrapper.
Who this is for
One person — the maintainer. Multi-tenancy, hosted SaaS, and group-bulk-messaging use cases are explicitly out of scope. The entire safety story assumes a single-user threat model where FileVault / LUKS is the encryption boundary and wa panic is the recovery button. If that's not what you want, use one of the alternatives listed in What this is NOT.
License
Apache-2.0. The go.mau.fi/whatsmeow upstream is MPL-2.0, which is file-level copyleft and does not propagate to consumers (Mozilla MPL FAQ Q9–Q11). The Apache choice matches the precedent set by Anthropic's official Telegram channel plugin and gives an explicit patent grant.
Acknowledgements
tulir/whatsmeow— Tulir Asokan and the mautrix project, for the only WhatsApp library worth using in 2026.AsamK/signal-cli— for proving that a daemon-plus-thin-CLI architecture for an end-to-end-encrypted messenger is achievable in a single binary.aldinokemal/go-whatsapp-web-multidevice— closest prior art, solving a different shape of the problem (REST gateway vs CLI daemon).spf13/cobra+creachadair/jrpc2+modernc.org/sqlite— the three load-bearing Go libraries that make this project CGO-free, testable, and pleasant to maintain.rogpeppe/go-internal— forlockedfileandtestscript, both of which the project leans on heavily.
See also (yolo-labz ecosystem)
- yolo-labz/claude-mac-chrome — Chrome automation for cross-app pipelines (e.g., LinkedIn DM → triage → WhatsApp follow-up via this daemon).
- yolo-labz/linkedin-chrome-copilot — LinkedIn workflow plugin that composes with
wafor cross-channel automation. - yolo-labz/kokoro-speakd — TTS daemon for spoken status feedback during long-running send batches.
- Architecture deep-dives + WhatsApp-daemon design rationale: blog.home301server.com.br.
- Author portfolio: portfolio.home301server.com.br.
Services
Compliance-grade AI architecture for regulated workloads — async-first, USD-denominated, LATAM-based / EN-fluent. See blog.home301server.com.br/services.