Claude Magic Link
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"). claude-magic-link --check-provider evaluates the delivered probe for you. 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]
[--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 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:
- Sender domain is
mail.anthropic.com - Link uses HTTPS
- Link host is exactly
claude.ai - Link path is
/magic-link - To-header matches the address encoded in the link
- 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
(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
- cc-telegram-bot: security-hardened Telegram bot for remote Claude Code access (24 security layers)
- lydia-bible-bot: AI Bible study assistant for Telegram groups
- ubuntu-server-security: server hardening (14 components, CIS Benchmark)
- bash-production-toolkit: production-ready Bash libraries
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_magic_link-0.5.0.tar.gz.
File metadata
- Download URL: claude_magic_link-0.5.0.tar.gz
- Upload date:
- Size: 33.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef9443322d7e190222fde3eb86608ea19138a8982e89d16dcc0ae06e8620b2df
|
|
| MD5 |
36b9322f91d57674c7e69426ab76f069
|
|
| BLAKE2b-256 |
077f5b6297880c1a0cb895744de10d5ff17da49d41e4a627e4001fb5d20b978a
|
Provenance
The following attestation bundles were made for claude_magic_link-0.5.0.tar.gz:
Publisher:
release.yml on fidpa/claude-magic-link
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_magic_link-0.5.0.tar.gz -
Subject digest:
ef9443322d7e190222fde3eb86608ea19138a8982e89d16dcc0ae06e8620b2df - Sigstore transparency entry: 2642016868
- Sigstore integration time:
-
Permalink:
fidpa/claude-magic-link@d20f797b1b4b070e3103903b831146568204c5ff -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/fidpa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d20f797b1b4b070e3103903b831146568204c5ff -
Trigger Event:
release
-
Statement type:
File details
Details for the file claude_magic_link-0.5.0-py3-none-any.whl.
File metadata
- Download URL: claude_magic_link-0.5.0-py3-none-any.whl
- Upload date:
- Size: 23.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61d7b3b0a29f0b6fa958f5f58fbe7088410d41704bb19753421a738af9c053cc
|
|
| MD5 |
f0a5ced5174ecea8c0c0a31dbc19eb22
|
|
| BLAKE2b-256 |
c423d69a01fb51fd5fa99575a26d0cf85dd34a699161542de7527498ae06ff57
|
Provenance
The following attestation bundles were made for claude_magic_link-0.5.0-py3-none-any.whl:
Publisher:
release.yml on fidpa/claude-magic-link
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_magic_link-0.5.0-py3-none-any.whl -
Subject digest:
61d7b3b0a29f0b6fa958f5f58fbe7088410d41704bb19753421a738af9c053cc - Sigstore transparency entry: 2642016943
- Sigstore integration time:
-
Permalink:
fidpa/claude-magic-link@d20f797b1b4b070e3103903b831146568204c5ff -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/fidpa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d20f797b1b4b070e3103903b831146568204c5ff -
Trigger Event:
release
-
Statement type: