Baileys

GitHub

Socket-based TS/JavaScript API for WhatsApp Web

RAW Rules

AGENTS.md

# AGENTS.md

Guide for AI coding agents (Claude Code, Cursor, Aider, Codex, Copilot Workspace, etc.) contributing to Baileys. Human contributors should also read this β€” there's nothing AI-specific in the conventions, only in the disclosure rules at the end.

If you are an AI agent driving this repo, read this file first, then `CODE_OF_CONDUCT.md` (specifically the AI policy section), then `SECURITY.md`.

## What Baileys is

A TypeScript WebSocket client for the WhatsApp Web protocol. No browser, no Selenium β€” it speaks the binary Noise/protobuf protocol directly. Used by thousands of downstream projects, so changes to public APIs and wire-level handling have wide blast radius.

The library is **dual-use**: legitimate automation, bots, and integrations on one side; spam, stalkerware, and ToS-breaking automation on the other. We do not accept contributions whose primary purpose is to enable abuse (mass messaging, evasion of WhatsApp's anti-spam, scraping users without consent). See `CODE_OF_CONDUCT.md`.

## Repository layout

```
src/
  Socket/          High-level socket β€” chats, groups, messages send/recv, newsletter, USync
  Signal/          Signal Protocol session/sender-key wrapping over libsignal-node
  Utils/           Decoding, media, auth state, retry, app-state sync, generics
  Types/           Public TypeScript types β€” touching these is a public-API change
  WABinary/        Binary node encoding/decoding
  WAUSync/         USync query protocols
  Defaults/        Constants (WA Web version, baileys version, default config)
  __tests__/       Jest unit + integration tests; e2e tests live in __tests__/e2e
WAProto/           Generated protobuf bindings β€” DO NOT hand-edit
Example/           Reference implementation in example.ts
proto-extract/     Tooling to refresh protobufs from WA Web
scripts/           Repo automation (version bumps, etc.)
.github/workflows/ CI β€” lint, test, e2e, build, release
```

`WAProto/index.js`, `WAProto/index.d.ts` are generated by `WAProto/GenerateStatics.sh`. If a change requires modifying them, regenerate via `npm run gen:protobuf` rather than editing by hand.

## Setup

This repo requires **Yarn 4** via Corepack. Yarn 1 / classic will fail noisily on `package.json` resolutions.

```bash
corepack enable
yarn install
```

Node β‰₯ 20 (enforced by `engines` and `preinstall`).

## Daily commands

| Task | Command |
|---|---|
| Install | `yarn install` |
| Build (lib + types) | `yarn build` |
| Type-check + lint | `yarn lint` |
| Auto-fix lint + format | `yarn lint:fix` |
| Format only | `yarn format` |
| Unit + integration tests | `yarn test` |
| End-to-end tests | `yarn test:e2e` (requires the bartender mock server, see below) |
| Run the example | `yarn example` |
| Regenerate protobufs | `yarn gen:protobuf` |
| Audit deps | `yarn npm audit --recursive` |

`yarn lint` runs `tsc` first, then ESLint. A green lint means no type errors.

## Code style

- **TypeScript strict** β€” `strict`, `strictNullChecks`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax` are all on. Don't disable them locally.
- **Tabs** for indentation, single quotes, no semicolons (Prettier-enforced).
- **No `any` in new code.** Existing `any`s in tests are tolerated as warnings, not invitations.
- **No comments that restate the code.** Comment the *why* β€” protocol quirks, WhatsApp-side behavior, non-obvious workarounds. Don't comment-narrate "// loop over messages".
- **No emojis in code or commit messages** unless the user explicitly asks.
- **Named exports** preferred. Default exports only where they already exist (e.g., `makeWASocket`).
- **Errors**: throw `Boom` (`@hapi/boom`) for protocol/HTTP-style errors so downstream code can branch on `.output.statusCode`. Plain `Error` for everything else.
- **Logging**: every code path that crosses an async boundary should accept a `logger: ILogger` (pino-compatible). Don't `console.log`.

## Idiomatic patterns

These are the patterns the existing code uses. Match them. New code that does the same thing differently will get review comments asking you to align.

### Errors β€” Boom with statusCode

```ts
import { Boom } from '@hapi/boom'

if (!sock.user) {
  throw new Boom('Not authenticated', { statusCode: 401 })
}

if (!isJidUser(jid)) {
  throw new Boom(`Invalid jid: ${jid}`, { statusCode: 400 })
}
```

Downstream code branches on `error.output.statusCode` to retry, log out, or surface to the user. Plain `throw new Error(...)` loses that signal. Reserve plain `Error` for genuinely internal invariants where no caller is expected to recover.

### Logging β€” structured, never positional

```ts
// good: object first, message last; reads cleanly in JSON logs
logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted')
logger.debug({ messageKey }, 'already requested resend')
logger.error({ err: error, opName }, 'failed to parse mex notification JSON')

// bad: string interpolation, untyped fields
logger.warn(`error 463 from ${attrs.from}`)
console.log('failed:', error)
```

Pino convention: errors go under `err`, not `error` or `e`. The structured object is the *first* argument so log processors can index it.

### JIDs β€” always go through the helpers

```ts
import { jidDecode, jidNormalizedUser, areJidsSameUser, isJidUser } from '../WABinary'

const decoded = jidDecode(rawJid)            // { user, server, device?, agent? } | undefined
const normalized = jidNormalizedUser(rawJid) // strips device/agent, lowercases
const same = areJidsSameUser(a, b)           // compare user portions only
```

Never split a JID with `.split('@')` or compare with `===`. JIDs carry device suffixes (`:0`, `:42`), agent fields, and LID/PN duality β€” string ops will silently miss matches and you'll ship a bug that only fires on multi-device accounts.

### Binary nodes β€” typed accessors

```ts
import { getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString } from '../WABinary'

const groupsNode = getBinaryNodeChild(result, 'groups')
if (!groupsNode) {
  throw new Boom('missing <groups> in iq response', { statusCode: 502 })
}

const groups = getBinaryNodeChildren(groupsNode, 'group') // BinaryNode[]
const text = getBinaryNodeChildString(node, 'body')        // string | undefined
const { attrs } = node                                     // typed Record<string, string>
```

Don't reach into `node.content` as an array directly β€” types are loose and the shape varies by stanza. The accessors handle the missing/single/array cases.

### Sending IQs β€” `query` with timeouts

```ts
const result = await sock.query({
  tag: 'iq',
  attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'w:profile:picture' },
  content: [{ tag: 'picture', attrs: { type: 'image', query: 'url' } }]
}, /* timeoutMs */ 15_000)
```

`query` auto-generates the stanza id, attaches a one-shot listener, and rejects on timeout. Don't write your own `sock.ws.send` + manual listener β€” you'll leak listeners on errors.

### Listening for incoming stanzas β€” `CB:` prefix

```ts
sock.ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
  const { attrs } = getBinaryNodeChild(node, 'dirty')!
  // ...
})

sock.ws.on('CB:notification,type:server_sync', handler)
```

The `CB:tag,attr:value` syntax routes by tag + attribute filter inside the websocket. This is how `messages-recv`, `chats`, `groups` listen β€” don't filter manually inside a generic `'message'` handler.

### Public events β€” `ev.on`, never call twice

```ts
sock.ev.on('messages.upsert', ({ messages, type }) => { ... })
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => { ... })
sock.ev.on('creds.update', saveCreds)
```

`saveCreds` (or any handler) must be deduped β€” registering twice means writing twice. The harness in `__tests__/e2e/helpers/test-client.ts` shows the cleanup pattern (`ev.off` in teardown).

### Async cleanup β€” bracket pattern

When you allocate a resource (timer, listener, ws subscription) inside a Promise, clean it up in *both* paths:

```ts
return new Promise<T>((resolve, reject) => {
  const timer = setTimeout(() => {
    cleanup()
    reject(new Boom('timed out', { statusCode: 408 }))
  }, timeoutMs)

  const cleanup = () => {
    clearTimeout(timer)
    sock.ev.off('event.name', handler)
  }

  const handler = (data: T) => {
    if (matches(data)) {
      cleanup()
      resolve(data)
    }
  }

  sock.ev.on('event.name', handler)
})
```

Half-cleanups are how this codebase grew its memory leaks. The bracket pattern (allocate β†’ cleanup defined β†’ both paths call it) is the fix.

### Imports β€” `verbatimModuleSyntax`

```ts
import type { WAMessage, WAUrlInfo } from '../Types'
import { Boom } from '@hapi/boom'
import { type BinaryNode, getBinaryNodeChild } from '../WABinary'
```

Type-only imports must be marked `import type` or inline-prefixed `type` β€” TS strict-mode `verbatimModuleSyntax` will fail the build otherwise. Don't merge a value import with a type import unless you actually use both at runtime.

### Optional chains over null guards

```ts
// good
const text = msg.message?.extendedTextMessage?.text ?? msg.message?.conversation
if (!sent?.key.id) return

// bad β€” proliferates `if (x && x.y && x.y.z)` ladders
if (msg.message && msg.message.extendedTextMessage) { ... }
```

`noUncheckedIndexedAccess` is on, so array/record indexing returns `T | undefined`. Don't paper over it with `!` unless you have an invariant the type system can't see β€” and if you do, leave a one-line comment explaining the invariant.

### Tests β€” colocate, name by behavior

```ts
// src/__tests__/Utils/decode-wa-message.test.ts
describe('SERVER_ERROR_CODES', () => {
  it('MessageAccountRestriction is 463', () => {
    expect(SERVER_ERROR_CODES.MessageAccountRestriction).toBe('463')
  })
})
```

Test files mirror the source path: `src/Foo/bar.ts` β†’ `src/__tests__/Foo/bar.test.ts`. `describe` names the unit, `it` names the behavior in plain English. Avoid `it('works')`.

## Public API discipline

`src/Types/**` and the top-level `src/index.ts` re-exports define the public surface. Treat changes there as breaking unless you can prove additive-only.

For wire-level changes (`Socket/messages-recv.ts`, `Utils/decode-wa-message.ts`, `WABinary/`, etc.) β€” describe the WhatsApp-side trigger in the PR. Reviewers can't always reproduce protocol behavior, so the description does the heavy lifting.

## Commits and PRs

Conventional commits, scoped where useful:

```
feat(socket): add support for reachout limits XWAs
fix(retry): process <keys> bundle and embed SKDM on resend
chore(deps): bump ajv from 6.12.6 to 6.15.0
test(e2e): test harness + signal/prekey fixes
```

PR titles follow the same convention. Squash-merge is the default.

Before opening a PR:
1. `yarn lint` β€” must be 0 errors.
2. `yarn test` β€” must be all green. If you can't run e2e locally, say so in the PR description.
3. Don't commit `baileys_auth_info/`, `.env`, `mitm_*.db`, `.superset/`, or any session state. Git is configured to ignore the obvious ones; double-check.
4. Don't commit regenerated `yarn.lock` from a different package manager. If your `yarn.lock` diff is unexpectedly large (thousands of lines for a one-line `package.json` change), you're using Yarn 1 β€” switch to Corepack.

## What not to touch without coordination

- **`libsignal` cryptographic flows** β€” session state, prekeys, sender-key derivation. Subtle bugs here are silent and brick downstream sessions.
- **`Defaults/baileys-version.json`** β€” bumped by the `update-version` workflow. Manual edits race with automation.
- **`WAProto/` generated files** β€” regenerate, don't hand-edit.
- **`.github/workflows/`** β€” CI changes are reviewed separately; bundle them in their own PR when possible.
- **`package.json` `resolutions`** β€” these patch known security advisories. Removing entries reintroduces CVEs; check `yarn npm audit --recursive` before pruning.

## Testing expectations

- New public API β†’ unit test in `src/__tests__/`.
- New protocol path or stanza handler β†’ integration test mocking the binary node, *not* an e2e test (e2e is expensive and flaky in agent loops).
- New crypto/auth flow β†’ e2e against bartender if feasible, otherwise a deterministic fixture-based unit test.

Tests are colocated by area: `src/__tests__/Socket/`, `src/__tests__/Utils/`, `src/__tests__/binary/`. Match the existing layout.

## Security-sensitive changes

If your change touches:
- Auth state read/write, key storage, prekey/session lifecycle
- Message decryption / signature verification
- Any path that handles user PII (phone numbers, JIDs, message content) in logs or errors

…flag it explicitly in the PR description. See `SECURITY.md` for disclosure of vulnerabilities (do not file them as public issues).

## AI agent etiquette

Beyond the AI policy in `CODE_OF_CONDUCT.md`:

- **Read before you write.** This codebase has subtle protocol invariants. A grep-and-replace agent will make a mess of `Socket/messages-recv.ts`. Read the surrounding handler before editing.
- **Don't invent WhatsApp protocol details.** If you're not sure how a stanza is structured, find a real example in the tests or in `Utils/decode-wa-message.ts`. Hallucinated XML attributes get merged and then break in production.
- **Stay in scope.** A bug fix doesn't need a refactor pass. A type tweak doesn't need a comment cleanup PR.
- **Don't paste auth state into AI tools.** `baileys_auth_info/` contains long-lived Signal keys. Treat it like an SSH private key.
- **Disclose AI authorship in PRs.** A one-line "drafted with [tool], reviewed by [human]" is enough. See `CODE_OF_CONDUCT.md` Β§ AI Policy for the full rule.

## Where to ask

- Discord: https://discord.gg/WeJM5FP9GG
- Wiki: https://baileys.wiki
- Security: see `SECURITY.md`

If you're an agent and you're stuck on something this file doesn't cover, fall back to reading the relevant `src/` directory and the matching tests β€” they're the source of truth.