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.

Claude.ai uses passwordless "magic link" authentication: every login sends an email, you open it, find the link, click it, confirm. With several accounts that round-trip gets old quickly.

claude-magic-link watches your IMAP mailbox via IDLE and opens the magic link in the right browser as soon as it arrives. The only step left is the confirmation click.

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
  • Verification: six checks before anything opens (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
  • macOS and Linux: browser launch via open -b or xdg-open
  • 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; if you installed from PyPI rather than 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

# One-time provider check (see "Testing your provider" in SECURITY.md)
claude-magic-link --check-provider

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

The From header alone is easy to forge. To make the tool demand a DMARC/DKIM pass recorded by your own mail server, set your provider's authserv-id:

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

With this set, a mail is only accepted if a header from your provider records a DMARC or DKIM pass for the Anthropic sender domain. Fail verdicts veto across all matching headers regardless of order, so a forged pass cannot outrank a genuine fail.

The check is only as strong as your provider's handling of forged Authentication-Results headers. Run the one-time test in SECURITY.md ("Testing your provider") before relying on it; claude-magic-link --check-provider evaluates the delivered probe for you. Left empty, this option is skipped and the remaining checks (sender domain, link host/path, recipient match) still apply.

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]
                         [--check-provider] [--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
  --check-provider     Evaluate the SECURITY.md onboarding probe in each
                       mailbox and exit (see "Testing your provider")
  --max-age MAX_AGE    Only process mails younger than N minutes (default: from config)
  --verbose, -v        Enable debug logging
  --version            show program's version number 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 turn this tool into a complete authentication bypass. The confirmation click stays manual so that no login happens without a human 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; a port would have to replace both.

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. Each login is a small detour through a mailbox, and across accounts and devices those detours add up. This tool cuts the flow down to that final click in the browser; the click itself is deliberate, see "Why no auto-confirm?" above.

See Also

Metadata

Release files for claude-magic-link 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for claude-magic-link 0.5.1
File Size Uploaded
claude_magic_link-0.5.1.tar.gz 34.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for claude-magic-link 0.5.1
File Interpreter ABI Platform
claude_magic_link-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 58.2 kB

Release history Release notifications | RSS feed

This release

0.5.1 This release

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release 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