Skip to main content

Claude Magic Link

PyPI CI License: MIT Python Platform No Dependencies Last Commit

Log in to Claude.ai without touching your inbox.

The problem: Claude.ai uses passwordless "magic link" authentication. Every login sends an email, you open it, find the link, click it, confirm. With multiple accounts this gets tedious fast.

The solution: claude-magic-link watches your IMAP mailbox via IDLE (push-based, near-instant) and opens the magic link in the right browser automatically. You just confirm in the browser.

2026-08-29 09:41:03 [INFO] [imap:you@example.com] Connected, listening via IDLE
2026-08-29 09:41:07 [INFO] [imap:you@example.com] Opened link for you@example.com in com.google.Chrome (3s old)

Features

  • IMAP IDLE: push-based, opens links within seconds of arrival
  • Multi-Account: watch multiple mailboxes, each with its own browser and, if needed, its own IMAP server
  • Security-First: 6-layer verification (sender, HTTPS, host, path, recipient match, DMARC/DKIM)
  • Zero Dependencies: pure Python standard library (3.14+)
  • Read-Only: never modifies, moves, or deletes your mail
  • Platform-Aware: macOS (open -b) and Linux (xdg-open) support
  • Desktop Notifications: macOS and Linux (notify-send)
  • Single Instance: flock-based lock prevents duplicate opens
  • Daemon Mode: run continuously with auto-reconnect and exponential backoff

Quick Start

Prerequisites

  • Python 3.14+ (for the IMAP IDLE API)
  • An IMAP mailbox that receives Claude.ai login emails
  • IMAP server with IDLE support (most providers have this)

Install

# From PyPI
pipx install claude-magic-link
# Later: pipx upgrade claude-magic-link

# Or from source
git clone https://github.com/fidpa/claude-magic-link.git
cd claude-magic-link
pip install .

Configure

Create the config at the platform-specific location and keep it private:

# macOS:
mkdir -p ~/Library/Application\ Support/claude-magic-link
$EDITOR ~/Library/Application\ Support/claude-magic-link/config.toml
chmod 600 ~/Library/Application\ Support/claude-magic-link/config.toml

# Linux:
mkdir -p ~/.config/claude-magic-link
$EDITOR ~/.config/claude-magic-link/config.toml
chmod 600 ~/.config/claude-magic-link/config.toml

A minimal config:

[imap]
host = "imap.example.com"

[[accounts]]
email = "you@example.com"
password_env = "CLAUDE_MAGIC_LINK_PASSWORD"
browser = "default"

Then provide the password: as an environment variable for a quick start, or via password_cmd (keychain or password manager, nothing in the environment; see Configuration):

export CLAUDE_MAGIC_LINK_PASSWORD="your-imap-password"

The full annotated reference is config.example.toml; without a clone, fetch it directly:

curl -O https://raw.githubusercontent.com/fidpa/claude-magic-link/main/config.example.toml

Run

# Test run (logs what would happen, opens nothing)
claude-magic-link --once --dry-run

# Single scan
claude-magic-link --once

# Continuous watching (daemon mode)
claude-magic-link

# With custom config location
claude-magic-link --config /path/to/config.toml

Configuration

See config.example.toml for the full reference. Key sections:

IMAP Server

[imap]
host = "imap.example.com"
port = 993
mailbox = "INBOX"   # optional; watch a dedicated folder instead

[imap] is the default server; an account can override it with its own host / port / mailbox to watch mailboxes at different providers from one instance.

mailbox (default INBOX) selects the folder the watcher opens, always read-only. Pair it with a server-side rule that files Anthropic mail into a dedicated folder and the watcher never opens the rest of the mailbox. This is hygiene, not a security boundary: the IMAP credential could still read everything, so a dedicated address remains the stronger scoping (see the FAQ).

Accounts

[[accounts]]
email = "you@example.com"
password_env = "CLAUDE_MAGIC_LINK_PASSWORD"
browser = "default"

[[accounts]]
email = "work@example.com"
password_cmd = "security find-generic-password -s claude-magic-link -w"
browser = "com.google.Chrome"
host = "imap.workmail.example"   # optional per-account override

The password comes from exactly one of three sources per account, checked in this order: password (inline), password_cmd (stdout of a command such as the macOS Keychain or pass, run once at startup without a shell; wrap in sh -c '...' for pipes), password_env (environment variable).

Each account maps an email address to a browser:

Value Platform Effect
"default" Both System default browser
"com.google.Chrome" macOS Chrome via bundle ID
"com.microsoft.edgemac" macOS Edge via bundle ID
"org.mozilla.firefox" macOS Firefox via bundle ID
"google-chrome" Linux Chrome via command
"firefox" Linux Firefox via command

These are examples, not a closed list: on macOS any value containing a dot is treated as a bundle ID (open -b), anything else runs as a command; on Linux every non-default value runs as a command.

DMARC/DKIM Verification

For maximum security, configure your mail provider's authserv-id:

[security]
trusted_authserv_suffix = "mailhosting.your-provider.com"

This verifies that incoming Anthropic mails actually passed DMARC/DKIM at your provider's mail server, preventing spoofed sender addresses. Fail verdicts veto across all matching headers regardless of header order, so a forged pass header cannot outrank a genuine fail recorded by your provider. The check is only as strong as your provider's handling of forged Authentication-Results headers; before relying on it, run the one-time provider fixture in SECURITY.md ("Testing your provider"). Without this option, the tool still checks sender domain, link host/path, and recipient matching.

To find your provider's authserv-id, check the Authentication-Results header of any email in your inbox. The first field before the semicolon is the authserv-id.

How It Works

┌──────────────┐     IMAP IDLE      ┌───────────────────┐
│ Mail server  │ ───── push ──────> │ claude-magic-link │
│ (Dovecot,    │                    │                   │
│  Exchange,   │                    │ 1. Extract link   │
│  Gmail, ...) │                    │ 2. Verify sender  │
└──────────────┘                    │ 3. Check DMARC    │
                                    │ 4. Match To addr  │
                                    │ 5. Dedup check    │
                                    │ 6. Open browser   │
                                    └─────────┬─────────┘
                                              │
                                    open -b / xdg-open
                                              │
                                              ▼
                                    ┌───────────────────┐
                                    │      Browser      │
                                    │   (you confirm)   │
                                    └───────────────────┘

Running as a Service

macOS (LaunchAgent)

LaunchAgents do not inherit your shell environment. The cleanest setup is password_cmd in the config (e.g. the macOS Keychain, as shown above): the agent then starts the binary directly and no password ever touches the plist or the environment. If you rely on password_env instead, point ProgramArguments at a small wrapper script that exports the variable(s) first (e.g. set -a; source ~/.config/claude-magic-link/env; set +a).

Create ~/Library/LaunchAgents/com.user.claude-magic-link.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.claude-magic-link</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/yourname/.local/bin/claude-magic-link</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <dict>
        <key>SuccessfulExit</key>
        <false/>
    </dict>
    <key>ThrottleInterval</key>
    <integer>60</integer>
    <key>StandardOutPath</key>
    <string>/tmp/claude-magic-link.stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/claude-magic-link.stderr.log</string>
</dict>
</plist>
# macOS 13+:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.claude-magic-link.plist
# Older versions:
launchctl load ~/Library/LaunchAgents/com.user.claude-magic-link.plist

Linux (systemd)

Create ~/.config/systemd/user/claude-magic-link.service:

[Unit]
Description=Claude Magic Link Watcher
After=network-online.target

[Service]
ExecStart=/path/to/claude-magic-link
# With password_cmd in the config (e.g. `pass show claude-magic-link`),
# no Environment= line is needed. For password_env, prefer a credential
# store or EnvironmentFile= over an inline value:
# Environment=CLAUDE_MAGIC_LINK_PASSWORD=your-password
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
systemctl --user enable --now claude-magic-link

CLI Reference

usage: claude-magic-link [-h] [--config CONFIG] [--once] [--dry-run]
                         [--max-age MAX_AGE] [--verbose] [--version]

options:
  --config, -c CONFIG  Path to config.toml (default: platform-specific location)
  --once               Single scan instead of continuous watching
  --dry-run            Log what would happen without opening anything
  --max-age MAX_AGE    Only process mails younger than N minutes (default: from config)
  --verbose, -v        Enable debug logging
  --version            Show version and exit

Files

Purpose macOS Linux
Config ~/Library/Application Support/claude-magic-link/config.toml ~/.config/claude-magic-link/config.toml (respects $XDG_CONFIG_HOME)
Dedup state ~/Library/Application Support/claude-magic-link/state/seen.json ~/.local/share/claude-magic-link/state/seen.json (respects $XDG_DATA_HOME)
Log (rotating, 1 MB, 3 backups) ~/Library/Logs/claude-magic-link.log ~/.local/share/claude-magic-link/claude-magic-link.log
Lock ~/Library/Application Support/claude-magic-link/claude-magic-link.lock ~/.local/share/claude-magic-link/claude-magic-link.lock

The dedup state remembers the last 200 handled Message-IDs, so a link is opened once even with several workers running. Delete seen.json to process an already-handled mail again.

Security

See SECURITY.md for the full security model. In short, every mail must pass 6 independent checks before a link is opened:

  1. Sender domain is mail.anthropic.com
  2. Link uses HTTPS
  3. Link host is exactly claude.ai
  4. Link path is /magic-link
  5. To-header matches the address encoded in the link
  6. DMARC/DKIM passes at your provider (when configured)

On top of the checks, two hardening limits apply: mails larger than 512 KB are skipped, and only mails younger than max_age_minutes (default: 10) are processed. A mail that fails verification is logged and triggers a desktop notification (once per mail); nothing is opened.

Design Decisions

Why IMAP IDLE instead of EWS/Graph API? IMAP IDLE is provider-agnostic: it works with Gmail, Dovecot, Exchange, Fastmail, and any standards-compliant server. EWS is Exchange-only and deprecated by Microsoft. Graph API requires Azure AD app registration. IMAP IDLE is the simplest path that works everywhere.

Why Python 3.14? The IMAP IDLE API (IMAP4.idle()) was added in Python 3.14. It handles the protocol correctly (DONE, tags, unsolicited responses) without third-party libraries. This keeps the project dependency-free.

Why no auto-confirm? Auto-confirming would make this tool a complete authentication bypass, which is a security risk. The manual confirmation step ensures a human is present.

Why EXAMINE instead of SELECT? The tool uses select(readonly=True), which sends the IMAP EXAMINE command. This guarantees the mailbox is never modified: no flags are set, no messages moved or deleted.

FAQ

Why not use the API instead? Claude.ai uses magic-link authentication for the web interface, not the API. The API uses API keys. This tool is for the web/desktop app login flow.

Does it work with Gmail? Yes, if you enable IMAP access and use an app-specific password. Gmail supports IMAP IDLE.

Does it work with OAuth/XOAUTH2? Not yet. The current version uses plain IMAP LOGIN. OAuth support would be a welcome contribution.

Why not just stay logged in? Sessions expire, and with several accounts spread across browsers, profiles, and devices you end up logging in regularly anyway. This tool removes the inbox round-trip from that flow; only the deliberate confirmation click remains.

Does it work on Windows? Not yet: the single-instance lock uses fcntl and browser launching uses open/xdg-open. Contributions welcome.

The mail arrives, but nothing opens (forwarded mailbox)? The watcher only sees mail that stays in the watched IMAP mailbox. If your provider forwards the login mail elsewhere without keeping a copy, IMAP never sees it. Configure the rule as "keep a copy" instead of plain forwarding, or watch the destination mailbox instead.

Does it need full mailbox access? The watcher opens a single folder (mailbox, default INBOX) strictly read-only, and its IMAP SEARCH is limited to recent mail from the trusted sender domain. But IMAP credentials are account-scoped, so the login itself could read everything; a folder filter does not change that. The strongest scoping is a dedicated address that receives nothing but Claude login mail, with its own credentials; the mailbox option plus a server-side filing rule is a lighter hygiene measure on top.

Development

git clone https://github.com/fidpa/claude-magic-link.git
cd claude-magic-link
python3.14 -m venv .venv
.venv/bin/pip install pytest ruff

.venv/bin/pytest        # run the test suite
.venv/bin/ruff check .  # lint

The test suite covers the security checks (including the attack cases from SECURITY.md), the IMAP helpers, config loading, and state persistence, all without a network connection.

License

MIT

(c) 2026 Marc Allgeier

Author

Marc Allgeier (@fidpa)

Why I Built This: I manage multiple Claude.ai accounts for work and personal use. The login flow (open email, find the link, click it, wait for the browser, confirm) is a minor friction that adds up fast. This tool eliminates everything except the final confirmation click, which intentionally remains as a security boundary.

See Also

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

claude_magic_link-0.4.1.tar.gz (31.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

claude_magic_link-0.4.1-py3-none-any.whl (21.7 kB view details)

Uploaded Python 3

Release history Release notifications | RSS feed

0.5.0

2 files

This release

0.4.1 This release

2 files

0.4.0

2 files

0.3.3

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page