Authentication Process Using QR Codes in wacli: How It Works and Why It Differs from Sync

The wacli auth command initiates an interactive QR code authentication flow that creates a new WhatsApp Web session, while the wacli sync command operates only on existing authenticated sessions and explicitly disables QR code prompts.

steipete/wacli is a command-line interface for WhatsApp Web that distinguishes between initial authentication and subsequent synchronization. The repository implements a clear separation between the interactive QR code login flow and non-interactive sync operations through distinct command implementations and strict state validation.

How QR Code Authentication Works in wacli

The authentication flow spans three architectural layers, from the CLI surface down to the low-level WhatsApp client implementation.

The Three-Layer Architecture

Layer Responsibility Key Source File
CLI command Starts the process, configures QR options, renders the QR code in the terminal using qrterminal. cmd/wacli/auth.go – the AllowQR: true flag and the OnQRCode callback
App wrapper Opens the WhatsApp client and forwards QR options to the underlying connection. internal/app/app.goConnect(ctx, allowQR, qrWriter) forwards to wa.Connect
WhatsApp client Connects to WhatsApp Web, obtains a QR channel when unauthenticated, streams events, and blocks until scan succeeds. internal/wa/client.goConnect implements the QR flow handling code, success, timeout, and error events

Step-by-Step Authentication Flow

  1. User invokes wacli auth
    The command builds a SyncOptions struct with AllowQR: true and an OnQRCode callback that prints the code via qrterminal.GenerateHalfBlock.

  2. App layer forwards options
    App.Sync calls a.Connect(ctx, true, qrWriter), which passes the boolean flag and writer to the low-level client.

  3. Client initializes QR channel
    Inside wa.Connect, the code checks if !authed && connectOptions.AllowQR. When true, it calls cli.GetQRChannel(ctx) to receive a channel of QR events.

  4. Event loop handles scan states
    After connecting to WhatsApp Web, the client enters a select loop reading from the QR channel:

    • "code": Invokes the OnQRCode callback to render the terminal QR code.
    • "success": Returns from the connect function, leaving the client authenticated.
    • "timeout" or "error": Aborts with an error indicating authentication failure.
  5. Bootstrap proceeds
    Once authenticated, App.Sync continues with the regular bootstrap process (downloading messages, contacts, and groups).

Key Differences Between auth and sync Commands

The sync command is explicitly designed as a post-authentication operation and enforces this constraint through code-level guards.

Interactive vs. Non-Interactive Operation

  • wacli auth: Interactive entry point. Sets AllowQR: true and provides the OnQRCode callback to enable visual QR code rendering in the terminal.
  • wacli sync: Non-interactive, repeatable job. Sets AllowQR: false and passes nil for the QR writer, ensuring the process fails immediately if no session exists rather than prompting for a scan.

Authentication Requirements

In cmd/wacli/sync.go, the command first calls a.EnsureAuthed(), which fails fast if the store does not contain a valid session:

if err := a.EnsureAuthed(); err != nil {
    return fmt.Errorf("not authenticated: %w", err)
}

The SyncOptions struct then explicitly disables QR:

res, err := a.Sync(ctx, appPkg.SyncOptions{
    Mode:            mode,
    AllowQR:         false,  // QR disabled
    DownloadMedia:   downloadMedia,
    // ...
})

If the client is unauthenticated when sync runs, wa.Connect returns an error instructing the user to run wacli auth first.

Implementation Details and Code Examples

QR Code Rendering in auth.go

The cmd/wacli/auth.go file defines the interactive authentication command:

res, err := a.Sync(ctx, appPkg.SyncOptions{
    Mode:            mode,
    AllowQR:         true,               // Enable QR flow
    DownloadMedia:   downloadMedia,
    RefreshContacts: true,
    RefreshGroups:   true,
    IdleExit:        idleExit,
    OnQRCode: func(code string) {          // Terminal QR renderer
        fmt.Fprintln(os.Stderr, "\nScan this QR code with WhatsApp (Linked Devices):")
        qrterminal.GenerateHalfBlock(code, qrterminal.M, os.Stderr)
        fmt.Fprintln(os.Stderr)
    },
})

Non-Interactive Sync in sync.go

The cmd/wacli/sync.go file disables QR and requires existing authentication:

// Ensure we have a stored session
if err := a.EnsureAuthed(); err != nil {
    return fmt.Errorf("not authenticated: %w", err)
}

// Run sync without QR support
res, err := a.Sync(ctx, appPkg.SyncOptions{
    Mode:            mode,
    AllowQR:         false,              // Explicitly disabled
    DownloadMedia:   downloadMedia,
    RefreshContacts: refreshContacts,
    RefreshGroups:   refreshGroups,
    IdleExit:        idleExit,
    MaxReconnect:    maxReconnect,
})

Low-Level Client Logic in client.go

The internal/wa/client.go file implements the event-driven QR flow:

func (c *Client) Connect(ctx context.Context, allowQR bool, qrWriter func(string)) error {
    // Check if already authenticated
    authed := c.Store.HasSession()
    
    var qrChan <-chan whatsmeow.QRChannelItem
    if !authed && allowQR {
        // Request QR channel only when unauthenticated and allowed
        qrChan, _ = c.cli.GetQRChannel(ctx)
    }
    
    // Connect to WhatsApp Web
    if err := c.cli.ConnectContext(ctx); err != nil {
        return err
    }
    
    // If we need QR authentication, wait for scan
    if qrChan != nil {
        for evt := range qrChan {
            switch evt.Event {
            case "code":
                // Render the QR code
                qrWriter(evt.Code)
            case "success":
                // Authentication complete
                return nil
            case "timeout":
                return fmt.Errorf("QR code expired")
            case "error":
                return fmt.Errorf("QR error: %v", evt.Error)
            }
        }
    }
    
    return nil
}

Summary

  • Interactive Authentication: The wacli auth command enables QR code login by setting AllowQR: true and providing an OnQRCode callback that renders the code in the terminal using qrterminal.
  • Three-Layer Flow: Authentication spans the CLI (cmd/wacli/auth.go), the app wrapper (internal/app/app.go), and the WhatsApp client (internal/wa/client.go).
  • Event-Driven Protocol: The client uses GetQRChannel to receive events (code, success, timeout, error) and blocks until the user scans the QR code with their mobile device.
  • Strict Separation: The wacli sync command disables QR (AllowQR: false) and calls EnsureAuthed() to enforce that authentication must precede synchronization, making it suitable for automated, non-interactive workflows.

Frequently Asked Questions

Can I use wacli sync without first running wacli auth?

No. The sync command explicitly checks for existing authentication via EnsureAuthed() and returns an error if no session exists in the store. You must run wacli auth first to establish the session via QR code.

What happens if I don't scan the QR code within the timeout period?

If the QR code expires, the internal/wa/client.go event loop receives a "timeout" event from the QR channel and returns an error, causing the wacli auth command to exit with a failure status. You must restart the command to generate a fresh QR code.

Where does wacli store authentication credentials after successful QR login?

The credentials are stored in the local database managed by the Store interface within the WhatsApp client. The client.go implementation checks c.Store.HasSession() to determine if authentication exists, and the sync.go command uses EnsureAuthed() to verify this state before proceeding.

Is the QR code authentication flow secure?

Yes, the implementation follows the official WhatsApp Web protocol. The QR code contains a temporary key exchange token that is only valid for a limited time (handled by the "timeout" event). The terminal rendering via qrterminal displays the code locally without transmitting it over networks, and the underlying whatsmeow library handles the cryptographic handshake once the mobile device scans the code.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →