Metadata-Version: 2.5
Name: claude-magic-link
Version: 0.5.0
Summary: Log in to Claude.ai without touching your inbox: magic links auto-open in the right browser via IMAP IDLE
Project-URL: Homepage, https://github.com/fidpa/claude-magic-link
Project-URL: Repository, https://github.com/fidpa/claude-magic-link
Project-URL: Issues, https://github.com/fidpa/claude-magic-link/issues
Author-email: Marc Allgeier <webinar@arni-gmbh.de>
License-Expression: MIT
License-File: LICENSE
Keywords: anthropic,authentication,claude,imap,magic-link
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Security
Classifier: Topic :: Utilities
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# Claude Magic Link

[![PyPI](https://img.shields.io/pypi/v/claude-magic-link)](https://pypi.org/project/claude-magic-link/)
[![CI](https://github.com/fidpa/claude-magic-link/actions/workflows/lint.yml/badge.svg)](https://github.com/fidpa/claude-magic-link/actions/workflows/lint.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Python](https://img.shields.io/badge/Python-3.14%2B-blue?logo=python)](https://www.python.org/)
[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey)]()
[![No Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)]()
![Last Commit](https://img.shields.io/github/last-commit/fidpa/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](https://docs.python.org/3.14/library/imaplib.html#imaplib.IMAP4.idle))
- An IMAP mailbox that receives Claude.ai login emails
- IMAP server with IDLE support (most providers have this)

### Install

```bash
# 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:

```bash
# 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:

```toml
[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](#configuration)):

```bash
export CLAUDE_MAGIC_LINK_PASSWORD="your-imap-password"
```

The full annotated reference is [`config.example.toml`](config.example.toml);
without a clone, fetch it directly:

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

### Run

```bash
# 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`](config.example.toml) for the full reference. Key sections:

### IMAP Server

```toml
[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

```toml
[[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:

```toml
[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](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
<?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>
```

```bash
# 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`:

```ini
[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
```

```bash
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](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

```bash
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](SECURITY.md)), the IMAP helpers, config loading, and state persistence, all without a network connection.

## License

[MIT](LICENSE)

(c) 2026 [Marc Allgeier](https://github.com/fidpa)

## Author

Marc Allgeier ([@fidpa](https://github.com/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](https://github.com/fidpa/cc-telegram-bot): security-hardened Telegram bot for remote Claude Code access (24 security layers)
- [lydia-bible-bot](https://github.com/fidpa/lydia-bible-bot): AI Bible study assistant for Telegram groups
- [ubuntu-server-security](https://github.com/fidpa/ubuntu-server-security): server hardening (14 components, CIS Benchmark)
- [bash-production-toolkit](https://github.com/fidpa/bash-production-toolkit): production-ready Bash libraries
