claude-magic-link 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_magic_link-0.2.0/.github/workflows/lint.yml +126 -0
- claude_magic_link-0.2.0/.github/workflows/release.yml +38 -0
- claude_magic_link-0.2.0/.gitignore +44 -0
- claude_magic_link-0.2.0/CHANGELOG.md +74 -0
- claude_magic_link-0.2.0/LICENSE +21 -0
- claude_magic_link-0.2.0/PKG-INFO +329 -0
- claude_magic_link-0.2.0/README.md +305 -0
- claude_magic_link-0.2.0/SECURITY.md +79 -0
- claude_magic_link-0.2.0/config.example.toml +64 -0
- claude_magic_link-0.2.0/pyproject.toml +57 -0
- claude_magic_link-0.2.0/src/claude_magic_link/__init__.py +3 -0
- claude_magic_link-0.2.0/src/claude_magic_link/__main__.py +3 -0
- claude_magic_link-0.2.0/src/claude_magic_link/cli.py +356 -0
- claude_magic_link-0.2.0/src/claude_magic_link/config.py +133 -0
- claude_magic_link-0.2.0/src/claude_magic_link/imap.py +276 -0
- claude_magic_link-0.2.0/src/claude_magic_link/opener.py +76 -0
- claude_magic_link-0.2.0/src/claude_magic_link/py.typed +0 -0
- claude_magic_link-0.2.0/src/claude_magic_link/security.py +107 -0
- claude_magic_link-0.2.0/src/claude_magic_link/state.py +52 -0
- claude_magic_link-0.2.0/tests/test_config.py +101 -0
- claude_magic_link-0.2.0/tests/test_imap.py +97 -0
- claude_magic_link-0.2.0/tests/test_security.py +227 -0
- claude_magic_link-0.2.0/tests/test_state.py +40 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
name: Lint
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
syntax:
|
|
11
|
+
name: Syntax Check
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.14"
|
|
18
|
+
allow-prereleases: true
|
|
19
|
+
- name: Compile all modules
|
|
20
|
+
run: python -m py_compile src/claude_magic_link/*.py
|
|
21
|
+
- name: Import check
|
|
22
|
+
run: |
|
|
23
|
+
python -c "import claude_magic_link; print('OK: ' + claude_magic_link.__version__)"
|
|
24
|
+
env:
|
|
25
|
+
PYTHONPATH: src
|
|
26
|
+
- name: CLI responds
|
|
27
|
+
run: python -m claude_magic_link --version
|
|
28
|
+
env:
|
|
29
|
+
PYTHONPATH: src
|
|
30
|
+
|
|
31
|
+
tests:
|
|
32
|
+
name: Tests
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.14"
|
|
39
|
+
allow-prereleases: true
|
|
40
|
+
- name: Install pytest
|
|
41
|
+
run: python -m pip install pytest
|
|
42
|
+
- name: Run tests
|
|
43
|
+
run: pytest -q
|
|
44
|
+
|
|
45
|
+
ruff:
|
|
46
|
+
name: Ruff
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
- uses: actions/setup-python@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: "3.14"
|
|
53
|
+
allow-prereleases: true
|
|
54
|
+
- name: Install ruff
|
|
55
|
+
run: python -m pip install ruff
|
|
56
|
+
- name: Ruff check
|
|
57
|
+
run: ruff check .
|
|
58
|
+
|
|
59
|
+
version:
|
|
60
|
+
name: Version Consistency
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- name: pyproject.toml == __init__.py
|
|
65
|
+
run: |
|
|
66
|
+
V_PYPROJECT=$(grep -m1 'version' pyproject.toml | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
|
67
|
+
V_INIT=$(grep '__version__' src/claude_magic_link/__init__.py | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
|
68
|
+
echo "pyproject.toml: $V_PYPROJECT"
|
|
69
|
+
echo "__init__.py: $V_INIT"
|
|
70
|
+
[ "$V_PYPROJECT" = "$V_INIT" ] || { echo "::error::Version mismatch"; exit 1; }
|
|
71
|
+
|
|
72
|
+
links:
|
|
73
|
+
name: Markdown Links
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
- name: Check relative links
|
|
78
|
+
run: |
|
|
79
|
+
python3 -c "
|
|
80
|
+
import re, os, glob, sys
|
|
81
|
+
bad = []
|
|
82
|
+
for f in glob.glob('*.md') + glob.glob('**/*.md', recursive=True):
|
|
83
|
+
base = os.path.dirname(f)
|
|
84
|
+
for m in re.finditer(r'\[[^\]]*\]\(([^)#\s]+)\)', open(f).read()):
|
|
85
|
+
t = m.group(1)
|
|
86
|
+
if t.startswith(('http', 'mailto:')):
|
|
87
|
+
continue
|
|
88
|
+
if not os.path.exists(os.path.normpath(os.path.join(base, t))):
|
|
89
|
+
bad.append((f, t))
|
|
90
|
+
if bad:
|
|
91
|
+
for f, t in bad:
|
|
92
|
+
print(f'::error file={f}::Broken link: {t}')
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
print('All relative links OK')
|
|
95
|
+
"
|
|
96
|
+
|
|
97
|
+
changelog:
|
|
98
|
+
name: Changelog Links
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v4
|
|
102
|
+
- name: Every version has a link definition
|
|
103
|
+
run: |
|
|
104
|
+
if [ ! -f CHANGELOG.md ]; then
|
|
105
|
+
echo "No CHANGELOG.md yet, skipping"
|
|
106
|
+
exit 0
|
|
107
|
+
fi
|
|
108
|
+
FAIL=0
|
|
109
|
+
for v in $(grep -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md | tr -d '#[] '); do
|
|
110
|
+
if ! grep -q "^\[$v\]:" CHANGELOG.md; then
|
|
111
|
+
echo "::error file=CHANGELOG.md::Missing link definition for $v"
|
|
112
|
+
FAIL=1
|
|
113
|
+
fi
|
|
114
|
+
done
|
|
115
|
+
exit $FAIL
|
|
116
|
+
|
|
117
|
+
gitleaks:
|
|
118
|
+
name: Secrets Scan
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
steps:
|
|
121
|
+
- uses: actions/checkout@v4
|
|
122
|
+
with:
|
|
123
|
+
fetch-depth: 0
|
|
124
|
+
- uses: gitleaks/gitleaks-action@v2
|
|
125
|
+
env:
|
|
126
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.14"
|
|
16
|
+
allow-prereleases: true
|
|
17
|
+
- name: Install build
|
|
18
|
+
run: python -m pip install build
|
|
19
|
+
- name: Build sdist and wheel
|
|
20
|
+
run: python -m build
|
|
21
|
+
- uses: actions/upload-artifact@v4
|
|
22
|
+
with:
|
|
23
|
+
name: dist
|
|
24
|
+
path: dist/
|
|
25
|
+
|
|
26
|
+
publish:
|
|
27
|
+
name: Publish to PyPI
|
|
28
|
+
needs: build
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment: pypi
|
|
31
|
+
permissions:
|
|
32
|
+
id-token: write
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/download-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
venv/
|
|
11
|
+
.venv/
|
|
12
|
+
|
|
13
|
+
# Linter / type-checker caches
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
|
|
17
|
+
# dotenv environment variable files
|
|
18
|
+
.env
|
|
19
|
+
.env.*
|
|
20
|
+
|
|
21
|
+
# Runtime config (contains IMAP credentials)
|
|
22
|
+
config.toml
|
|
23
|
+
|
|
24
|
+
# Logs
|
|
25
|
+
*.log
|
|
26
|
+
|
|
27
|
+
# Personal / marketing content (not part of the project)
|
|
28
|
+
*_POST.md
|
|
29
|
+
|
|
30
|
+
# Private working instructions for Claude Code, not for the public repo
|
|
31
|
+
CLAUDE.md
|
|
32
|
+
|
|
33
|
+
# Local agent tooling (slash commands, session settings)
|
|
34
|
+
.claude/
|
|
35
|
+
|
|
36
|
+
# IDE / tooling
|
|
37
|
+
.idea/
|
|
38
|
+
.vscode/
|
|
39
|
+
*.swp
|
|
40
|
+
*.swo
|
|
41
|
+
*~
|
|
42
|
+
|
|
43
|
+
# OS
|
|
44
|
+
.DS_Store
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- **Exact token matching in DMARC/DKIM result parsing** -- the previous
|
|
14
|
+
substring match accepted lookalike domains such as
|
|
15
|
+
`header.d=mail.anthropic.com.evil.com` as a pass for `mail.anthropic.com`
|
|
16
|
+
- **Strict base64 validation for the recipient address in the link fragment**
|
|
17
|
+
-- invalid input previously decoded to an empty string instead of being
|
|
18
|
+
rejected; URL-safe base64 is now decoded correctly
|
|
19
|
+
- Startup warning when the config file contains an inline password but is
|
|
20
|
+
readable by other users
|
|
21
|
+
- State, log, and lock directories are created with mode 0700
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Mails whose INTERNALDATE is slightly in the future (server clock ahead of
|
|
25
|
+
the local clock) are no longer dropped; skew up to 120 s is clamped so
|
|
26
|
+
links open reliably
|
|
27
|
+
- IMAP SEARCH filter now derives from `trusted_sender_domains` instead of a
|
|
28
|
+
hardcoded domain, so custom sender domains are actually found
|
|
29
|
+
- Duplicate error output when the config file is missing
|
|
30
|
+
- macOS notifications now render non-ASCII text correctly
|
|
31
|
+
- Mails without a decodable body no longer raise an unhandled error
|
|
32
|
+
- Lock file is no longer unlinked on shutdown, closing a race in which two
|
|
33
|
+
instances could hold locks on different inodes of the same path
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- **Test suite** (pytest, 71 tests) covering the security checks including
|
|
37
|
+
the documented attack cases, IMAP helpers, config loading, and state
|
|
38
|
+
persistence
|
|
39
|
+
- **Ruff lint job and test job** in CI
|
|
40
|
+
- **PyPI publishing workflow** via GitHub Actions Trusted Publishing,
|
|
41
|
+
triggered by published GitHub releases
|
|
42
|
+
- Ruff configuration and dev dependency group in `pyproject.toml`
|
|
43
|
+
- FAQ entries: why not stay logged in, Windows support status
|
|
44
|
+
|
|
45
|
+
### Removed
|
|
46
|
+
- Internal agent tooling (`.claude/`) from the public repository
|
|
47
|
+
|
|
48
|
+
## [0.1.0] - 2026-08-29
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
- **IMAP IDLE watcher** for real-time magic link detection (Python 3.14+ `IMAP4.idle()` API)
|
|
52
|
+
- **Multi-account support** with per-account browser mapping (macOS bundle IDs and Linux commands)
|
|
53
|
+
- **6-layer security model**: sender domain, HTTPS, host, path, recipient matching, DMARC/DKIM verification
|
|
54
|
+
- **Per-result DKIM parsing** preventing cross-domain authentication bypass
|
|
55
|
+
- **Recipient validation** rejecting links with undecodable fragment addresses
|
|
56
|
+
- **Atomic deduplication** via `_try_reserve()` preventing race conditions across IMAP workers
|
|
57
|
+
- **Daemon mode** with auto-reconnect, exponential backoff, and graceful shutdown (SIGINT/SIGTERM)
|
|
58
|
+
- **Single-scan mode** (`--once`) for cron-style operation
|
|
59
|
+
- **Dry-run mode** (`--dry-run`) for safe testing
|
|
60
|
+
- **Desktop notifications** via osascript (macOS) and notify-send (Linux)
|
|
61
|
+
- **Single-instance locking** via flock in a user-private directory
|
|
62
|
+
- **Rotating log files** (1 MB, 3 backups) in platform-specific locations
|
|
63
|
+
- **Atomic state persistence** with temp-file write and rename
|
|
64
|
+
- **Zero dependencies** -- pure Python standard library
|
|
65
|
+
- **CLI** with argparse (`--config`, `--once`, `--dry-run`, `--max-age`, `--verbose`, `--version`)
|
|
66
|
+
- **Platform-aware paths** for config, state, logs, and lock files (macOS/Linux)
|
|
67
|
+
- **py.typed marker** for downstream type checking
|
|
68
|
+
- **CI** with 5 jobs: syntax check, version consistency, markdown links, changelog links, gitleaks
|
|
69
|
+
- **LaunchAgent and systemd examples** in README
|
|
70
|
+
- **Security documentation** (SECURITY.md) with threat model and recommendations
|
|
71
|
+
|
|
72
|
+
[Unreleased]: https://github.com/fidpa/claude-magic-link/compare/v0.2.0...HEAD
|
|
73
|
+
[0.2.0]: https://github.com/fidpa/claude-magic-link/compare/v0.1.0...v0.2.0
|
|
74
|
+
[0.1.0]: https://github.com/fidpa/claude-magic-link/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marc Allgeier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: claude-magic-link
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Auto-open Claude.ai magic links from your inbox via IMAP IDLE
|
|
5
|
+
Project-URL: Homepage, https://github.com/fidpa/claude-magic-link
|
|
6
|
+
Project-URL: Repository, https://github.com/fidpa/claude-magic-link
|
|
7
|
+
Project-URL: Issues, https://github.com/fidpa/claude-magic-link/issues
|
|
8
|
+
Author-email: Marc Allgeier <webinar@arni-gmbh.de>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: anthropic,authentication,claude,imap,magic-link
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Communications :: Email
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.14
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Claude Magic Link
|
|
26
|
+
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
[](https://www.python.org/)
|
|
29
|
+
[]()
|
|
30
|
+
[]()
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
Auto-open Claude.ai magic links from your inbox.
|
|
34
|
+
|
|
35
|
+
**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.
|
|
36
|
+
|
|
37
|
+
**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.
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **IMAP IDLE** -- push-based, opens links within seconds of arrival
|
|
42
|
+
- **Multi-Account** -- watch multiple mailboxes, each with its own browser
|
|
43
|
+
- **Security-First** -- 6-layer verification (sender, HTTPS, host, path, recipient match, DMARC/DKIM)
|
|
44
|
+
- **Zero Dependencies** -- pure Python standard library (3.14+)
|
|
45
|
+
- **Read-Only** -- never modifies, moves, or deletes your mail
|
|
46
|
+
- **Platform-Aware** -- macOS (`open -b`) and Linux (`xdg-open`) support
|
|
47
|
+
- **Desktop Notifications** -- macOS and Linux (notify-send)
|
|
48
|
+
- **Single Instance** -- flock-based lock prevents duplicate opens
|
|
49
|
+
- **Daemon Mode** -- run continuously with auto-reconnect and exponential backoff
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### Prerequisites
|
|
54
|
+
|
|
55
|
+
- Python 3.14+ (for the [IMAP IDLE API](https://docs.python.org/3.14/library/imaplib.html#imaplib.IMAP4.idle))
|
|
56
|
+
- An IMAP mailbox that receives Claude.ai login emails
|
|
57
|
+
- IMAP server with IDLE support (most providers have this)
|
|
58
|
+
|
|
59
|
+
### Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# From PyPI
|
|
63
|
+
pipx install claude-magic-link
|
|
64
|
+
|
|
65
|
+
# Or from source
|
|
66
|
+
git clone https://github.com/fidpa/claude-magic-link.git
|
|
67
|
+
cd claude-magic-link
|
|
68
|
+
pip install .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Configure
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Copy the example config to the platform-specific location
|
|
75
|
+
# macOS:
|
|
76
|
+
mkdir -p ~/Library/Application\ Support/claude-magic-link
|
|
77
|
+
cp config.example.toml ~/Library/Application\ Support/claude-magic-link/config.toml
|
|
78
|
+
|
|
79
|
+
# Linux:
|
|
80
|
+
mkdir -p ~/.config/claude-magic-link
|
|
81
|
+
cp config.example.toml ~/.config/claude-magic-link/config.toml
|
|
82
|
+
|
|
83
|
+
# Edit the config
|
|
84
|
+
$EDITOR ~/Library/Application\ Support/claude-magic-link/config.toml # macOS
|
|
85
|
+
$EDITOR ~/.config/claude-magic-link/config.toml # Linux
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Set your IMAP password as an environment variable:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
export CLAUDE_MAGIC_LINK_PASSWORD="your-imap-password"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Run
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Test run (logs what would happen, opens nothing)
|
|
98
|
+
claude-magic-link --once --dry-run
|
|
99
|
+
|
|
100
|
+
# Single scan
|
|
101
|
+
claude-magic-link --once
|
|
102
|
+
|
|
103
|
+
# Continuous watching (daemon mode)
|
|
104
|
+
claude-magic-link
|
|
105
|
+
|
|
106
|
+
# With custom config location
|
|
107
|
+
claude-magic-link --config /path/to/config.toml
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Configuration
|
|
111
|
+
|
|
112
|
+
See [`config.example.toml`](config.example.toml) for the full reference. Key sections:
|
|
113
|
+
|
|
114
|
+
### IMAP Server
|
|
115
|
+
|
|
116
|
+
```toml
|
|
117
|
+
[imap]
|
|
118
|
+
host = "imap.example.com"
|
|
119
|
+
port = 993
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Accounts
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
[[accounts]]
|
|
126
|
+
email = "you@example.com"
|
|
127
|
+
password_env = "CLAUDE_MAGIC_LINK_PASSWORD"
|
|
128
|
+
browser = "default"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Each account maps an email address to a browser:
|
|
132
|
+
|
|
133
|
+
| Value | Platform | Effect |
|
|
134
|
+
|-------|----------|--------|
|
|
135
|
+
| `"default"` | Both | System default browser |
|
|
136
|
+
| `"com.google.Chrome"` | macOS | Chrome via bundle ID |
|
|
137
|
+
| `"com.microsoft.edgemac"` | macOS | Edge via bundle ID |
|
|
138
|
+
| `"org.mozilla.firefox"` | macOS | Firefox via bundle ID |
|
|
139
|
+
| `"google-chrome"` | Linux | Chrome via command |
|
|
140
|
+
| `"firefox"` | Linux | Firefox via command |
|
|
141
|
+
|
|
142
|
+
### DMARC/DKIM Verification
|
|
143
|
+
|
|
144
|
+
For maximum security, configure your mail provider's authserv-id:
|
|
145
|
+
|
|
146
|
+
```toml
|
|
147
|
+
[security]
|
|
148
|
+
trusted_authserv_suffix = "mailhosting.your-provider.com"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This verifies that incoming Anthropic mails actually passed DMARC/DKIM at your provider's mail server, preventing spoofed sender addresses. Without this, the tool still checks sender domain, link host/path, and recipient matching.
|
|
152
|
+
|
|
153
|
+
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.
|
|
154
|
+
|
|
155
|
+
## How It Works
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
┌─────────────┐ IMAP IDLE ┌──────────────────┐
|
|
159
|
+
│ Mail Server │ ──── push ─────> │ claude-magic-link │
|
|
160
|
+
│ (Dovecot, │ │ │
|
|
161
|
+
│ Exchange, │ │ 1. Extract link │
|
|
162
|
+
│ Gmail...) │ │ 2. Verify sender │
|
|
163
|
+
│ │ │ 3. Check DMARC │
|
|
164
|
+
└─────────────┘ │ 4. Match To addr │
|
|
165
|
+
│ 5. Dedup check │
|
|
166
|
+
│ 6. Open browser │
|
|
167
|
+
└───────┬──────────┘
|
|
168
|
+
│
|
|
169
|
+
open -b / xdg-open
|
|
170
|
+
│
|
|
171
|
+
▼
|
|
172
|
+
┌──────────────┐
|
|
173
|
+
│ Browser │
|
|
174
|
+
│ (you confirm) │
|
|
175
|
+
└──────────────┘
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Running as a Service
|
|
179
|
+
|
|
180
|
+
### macOS (LaunchAgent)
|
|
181
|
+
|
|
182
|
+
Create `~/Library/LaunchAgents/com.user.claude-magic-link.plist`:
|
|
183
|
+
|
|
184
|
+
```xml
|
|
185
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
186
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
187
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
188
|
+
<plist version="1.0">
|
|
189
|
+
<dict>
|
|
190
|
+
<key>Label</key>
|
|
191
|
+
<string>com.user.claude-magic-link</string>
|
|
192
|
+
<key>ProgramArguments</key>
|
|
193
|
+
<array>
|
|
194
|
+
<string>/path/to/claude-magic-link</string>
|
|
195
|
+
</array>
|
|
196
|
+
<!-- Use a wrapper script loading from macOS Keychain for production -->
|
|
197
|
+
<key>EnvironmentVariables</key>
|
|
198
|
+
<dict>
|
|
199
|
+
<key>CLAUDE_MAGIC_LINK_PASSWORD</key>
|
|
200
|
+
<string>your-password</string>
|
|
201
|
+
</dict>
|
|
202
|
+
<key>RunAtLoad</key>
|
|
203
|
+
<true/>
|
|
204
|
+
<key>KeepAlive</key>
|
|
205
|
+
<true/>
|
|
206
|
+
<key>StandardOutPath</key>
|
|
207
|
+
<string>/tmp/claude-magic-link.stdout.log</string>
|
|
208
|
+
<key>StandardErrorPath</key>
|
|
209
|
+
<string>/tmp/claude-magic-link.stderr.log</string>
|
|
210
|
+
</dict>
|
|
211
|
+
</plist>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# macOS 13+:
|
|
216
|
+
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.claude-magic-link.plist
|
|
217
|
+
# Older versions:
|
|
218
|
+
launchctl load ~/Library/LaunchAgents/com.user.claude-magic-link.plist
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Linux (systemd)
|
|
222
|
+
|
|
223
|
+
Create `~/.config/systemd/user/claude-magic-link.service`:
|
|
224
|
+
|
|
225
|
+
```ini
|
|
226
|
+
[Unit]
|
|
227
|
+
Description=Claude Magic Link Watcher
|
|
228
|
+
After=network-online.target
|
|
229
|
+
|
|
230
|
+
[Service]
|
|
231
|
+
ExecStart=/path/to/claude-magic-link
|
|
232
|
+
# For production, load the password from a credential store instead of plaintext
|
|
233
|
+
Environment=CLAUDE_MAGIC_LINK_PASSWORD=your-password
|
|
234
|
+
Restart=always
|
|
235
|
+
RestartSec=10
|
|
236
|
+
|
|
237
|
+
[Install]
|
|
238
|
+
WantedBy=default.target
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
systemctl --user enable --now claude-magic-link
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## CLI Reference
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
usage: claude-magic-link [-h] [--config PATH] [--once] [--dry-run]
|
|
249
|
+
[--max-age N] [--verbose] [--version]
|
|
250
|
+
|
|
251
|
+
options:
|
|
252
|
+
--config, -c PATH Path to config.toml
|
|
253
|
+
--once Single scan instead of continuous watching
|
|
254
|
+
--dry-run Log what would happen without opening anything
|
|
255
|
+
--max-age N Only process mails younger than N minutes
|
|
256
|
+
--verbose, -v Enable debug logging
|
|
257
|
+
--version Show version and exit
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Security
|
|
261
|
+
|
|
262
|
+
See [SECURITY.md](SECURITY.md) for the full security model. In short, every mail must pass 6 independent checks before a link is opened:
|
|
263
|
+
|
|
264
|
+
1. Sender domain is `mail.anthropic.com`
|
|
265
|
+
2. Link uses HTTPS
|
|
266
|
+
3. Link host is exactly `claude.ai`
|
|
267
|
+
4. Link path is `/magic-link`
|
|
268
|
+
5. To-header matches the address encoded in the link
|
|
269
|
+
6. DMARC/DKIM passes at your provider (when configured)
|
|
270
|
+
|
|
271
|
+
## Design Decisions
|
|
272
|
+
|
|
273
|
+
**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.
|
|
274
|
+
|
|
275
|
+
**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.
|
|
276
|
+
|
|
277
|
+
**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.
|
|
278
|
+
|
|
279
|
+
**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.
|
|
280
|
+
|
|
281
|
+
## FAQ
|
|
282
|
+
|
|
283
|
+
**Why not use the API instead?**
|
|
284
|
+
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.
|
|
285
|
+
|
|
286
|
+
**Does it work with Gmail?**
|
|
287
|
+
Yes, if you enable IMAP access and use an app-specific password. Gmail supports IMAP IDLE.
|
|
288
|
+
|
|
289
|
+
**Does it work with OAuth/XOAUTH2?**
|
|
290
|
+
Not yet. The current version uses plain IMAP LOGIN. OAuth support would be a welcome contribution.
|
|
291
|
+
|
|
292
|
+
**Why not just stay logged in?**
|
|
293
|
+
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.
|
|
294
|
+
|
|
295
|
+
**Does it work on Windows?**
|
|
296
|
+
Not yet -- the single-instance lock uses `fcntl` and browser launching uses `open`/`xdg-open`. Contributions welcome.
|
|
297
|
+
|
|
298
|
+
## Development
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
git clone https://github.com/fidpa/claude-magic-link.git
|
|
302
|
+
cd claude-magic-link
|
|
303
|
+
python3.14 -m venv .venv
|
|
304
|
+
.venv/bin/pip install pytest ruff
|
|
305
|
+
|
|
306
|
+
.venv/bin/pytest # run the test suite
|
|
307
|
+
.venv/bin/ruff check . # lint
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
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.
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
[MIT](LICENSE)
|
|
315
|
+
|
|
316
|
+
(c) 2026 [Marc Allgeier](https://github.com/fidpa)
|
|
317
|
+
|
|
318
|
+
## Author
|
|
319
|
+
|
|
320
|
+
Marc Allgeier ([@fidpa](https://github.com/fidpa))
|
|
321
|
+
|
|
322
|
+
**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.
|
|
323
|
+
|
|
324
|
+
## See Also
|
|
325
|
+
|
|
326
|
+
- [cc-telegram-bot](https://github.com/fidpa/cc-telegram-bot) -- Security-hardened Telegram bot for remote Claude Code access (24 security layers)
|
|
327
|
+
- [lydia-bible-bot](https://github.com/fidpa/lydia-bible-bot) -- AI Bible study assistant for Telegram groups
|
|
328
|
+
- [ubuntu-server-security](https://github.com/fidpa/ubuntu-server-security) -- Server hardening (14 components, CIS Benchmark)
|
|
329
|
+
- [bash-production-toolkit](https://github.com/fidpa/bash-production-toolkit) -- Production-ready Bash libraries
|