multi-claude-cli 0.14.0__tar.gz → 0.16.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.
Files changed (74) hide show
  1. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/PKG-INFO +70 -13
  2. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/README.md +68 -12
  3. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/pyproject.toml +1 -0
  4. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/__init__.py +1 -1
  5. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/__main__.py +7 -0
  6. multi_claude_cli-0.16.0/src/multi_claude/_entry.py +43 -0
  7. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/accounts.py +21 -1
  8. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/artifacts.py +4 -5
  9. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/bundle.py +11 -7
  10. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/cli.py +129 -20
  11. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/completion.py +97 -2
  12. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/config.py +19 -1
  13. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/doctor.py +18 -14
  14. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/env.py +24 -1
  15. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/fsutil.py +31 -6
  16. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/history.py +5 -1
  17. multi_claude_cli-0.16.0/src/multi_claude/hook.py +185 -0
  18. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/identity.py +27 -1
  19. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/launcher.py +127 -5
  20. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/lock.py +5 -6
  21. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/migrate.py +62 -8
  22. multi_claude_cli-0.16.0/src/multi_claude/netcheck.py +642 -0
  23. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/plan.py +6 -2
  24. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/platform.py +469 -15
  25. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/restore.py +24 -3
  26. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/routes.py +33 -5
  27. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/sessions.py +5 -5
  28. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/shared.py +27 -2
  29. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/shellpath.py +20 -2
  30. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/statusline.py +34 -5
  31. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/PKG-INFO +70 -13
  32. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/SOURCES.txt +5 -1
  33. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_help_examples.py +4 -1
  34. multi_claude_cli-0.16.0/tests/test_netcheck.py +486 -0
  35. multi_claude_cli-0.16.0/tests/test_windows.py +502 -0
  36. multi_claude_cli-0.14.0/src/multi_claude/hook.py +0 -90
  37. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/LICENSE +0 -0
  38. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/setup.cfg +0 -0
  39. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/actions.py +0 -0
  40. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/refresh.py +0 -0
  41. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude/usage.py +0 -0
  42. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
  43. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
  44. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
  45. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_accounts.py +0 -0
  46. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_artifacts.py +0 -0
  47. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_bundle.py +0 -0
  48. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_completion.py +0 -0
  49. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_config.py +0 -0
  50. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_doctor.py +0 -0
  51. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_everyday.py +0 -0
  52. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_fsutil.py +0 -0
  53. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_handoff.py +0 -0
  54. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_help.py +0 -0
  55. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_history.py +0 -0
  56. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_identity.py +0 -0
  57. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_install.py +0 -0
  58. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_mcp.py +0 -0
  59. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_migrate.py +0 -0
  60. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_onboarding.py +0 -0
  61. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_packaging.py +0 -0
  62. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_plan.py +0 -0
  63. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_platform.py +0 -0
  64. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_refresh.py +0 -0
  65. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_rename.py +0 -0
  66. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_renewal.py +0 -0
  67. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_routes.py +0 -0
  68. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_sessions.py +0 -0
  69. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_set.py +0 -0
  70. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_shared_exclude.py +0 -0
  71. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_statusline.py +0 -0
  72. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_usability.py +0 -0
  73. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_usage.py +0 -0
  74. {multi_claude_cli-0.14.0 → multi_claude_cli-0.16.0}/tests/test_vscode.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multi-claude-cli
3
- Version: 0.14.0
3
+ Version: 0.16.0
4
4
  Summary: Run several Claude Code accounts side by side: separate CLAUDE_CONFIG_DIR directories, launchers, proxies, environment variables and arguments.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/jakoes-wu/multi-claude
@@ -11,6 +11,7 @@ Classifier: Environment :: Console
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows
14
15
  Classifier: Operating System :: POSIX :: Linux
15
16
  Classifier: Programming Language :: Python :: 3
16
17
  Classifier: Programming Language :: Python :: 3.8
@@ -27,17 +28,18 @@ Dynamic: license-file
27
28
 
28
29
  # multi-claude
29
30
 
30
- **English** | [简体中文](README.zh-CN.md)
31
+ **English** | [简体中文](https://github.com/jakoes-wu/multi-claude/blob/main/README.zh-CN.md)
31
32
 
32
33
  [![Release](https://img.shields.io/github/v/release/jakoes-wu/multi-claude)](https://github.com/jakoes-wu/multi-claude/releases)
33
34
  [![CI](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml/badge.svg)](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml)
34
35
  [![PyPI](https://img.shields.io/pypi/v/multi-claude-cli)](https://pypi.org/project/multi-claude-cli/)
35
36
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
36
- [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
38
+ ![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)
37
39
 
38
40
  Use several [Claude Code](https://code.claude.com/docs) accounts on one machine, at the same time. Each account keeps its own login, settings and history and, if you like, its own proxy. No more logging out and in again.
39
41
 
40
- It never copies or exports your login and never edits your shell startup files. Only `list` reads an account's access token and goes online, to ask Anthropic for the current plan (renewing the token first if it has expired); `list --offline` skips all of that.
42
+ It never copies or exports your login and never edits your shell startup files. Only `list` reads an account's access token and goes online, to ask Anthropic for the current plan (renewing the token first if it has expired); `list --offline` skips all of that. `doctor --network` also goes online, but only when you ask for it and without any login.
41
43
 
42
44
  ```sh
43
45
  claude-work # Claude Code with your work account
@@ -65,7 +67,7 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
65
67
  ## Features
66
68
 
67
69
  - **Your login stays yours** — logins are never copied or exported. One command touches them: `list` takes each account's access token (from the keychain, or `.credentials.json` on Linux) to ask Anthropic which plan the account is on, and when that token has expired it renews it the way Claude Code does and saves the new one in the same place. Tokens stay in memory and go only to Anthropic (`api.anthropic.com`, and `platform.claude.com` for a renewal), through that account's proxy. `list --no-refresh` never writes, `list --offline` reads nothing. Every other command only checks whether a login exists. `migrate-default` and `restore` move whole account directories, and on Linux the credentials file moves with its directory without being opened.
68
- - **Only `list` goes online** — its plan lookup (one request per account, plus one to renew an expired token) is the only connection multi-claude makes: no telemetry, no usage reporting, no update checks. `list --offline` makes none. Only the installer, pipx or Homebrew download the package.
70
+ - **Only `list` and `doctor --network` go online** — the plan lookup of `list` (one request per account, plus one to renew an expired token) and the connection check of `doctor --network` (one `HEAD /` without credentials to `api.anthropic.com` and `platform.claude.com` per proxy, repeated once if it fails) are the only connections multi-claude makes: no telemetry, no usage reporting, no update checks. `list --offline` makes none. Only the installer, pipx or Homebrew download the package.
69
71
  - **Leaves your shell alone** — when `PATH` needs a change, it prints the line to add instead of editing `~/.zshrc` or `~/.bashrc`.
70
72
  - **Changes only what you ask** — it writes its own configuration and state, the launchers, account directories (including the VS Code data that `code` keeps in `<root>/.apps`) and shared links. A Claude Code settings file is changed only by `statusline install` (the file you name, with a backup) and `add --config-from` (the new account's `settings.json`).
71
73
  - **Migrate the default account** — move your existing `~/.claude` into the account root without logging out. `claude` keeps working exactly as before.
@@ -78,13 +80,13 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
78
80
 
79
81
  ## Prerequisites
80
82
 
81
- **Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups and token renewals of `list`, sent to Anthropic.
83
+ **Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups and token renewals of `list` and the connection check of `doctor --network`, all sent to Anthropic.
82
84
 
83
85
  **Your computer.**
84
86
 
85
87
  | | Needed | Optional |
86
88
  | ---- | ---- | ---- |
87
- | Operating system | macOS or Linux (WSL counts as Linux) | — |
89
+ | Operating system | macOS, Linux (WSL counts as Linux) or Windows | — |
88
90
  | Tools | Python 3.8+ (`python3`, standard library only); Claude Code (`claude`); `curl` or `wget`, and `tar`, for the installer | `pipx` or Homebrew, to install from PyPI or the tap; `lsof`, so that `migrate-default` and `restore` can find processes using `~/.claude` (Linux falls back to `/proc`); VS Code's `code` command, for `multi-claude code` |
89
91
 
90
92
  Install what is missing:
@@ -100,7 +102,16 @@ sudo apt install -y python3 curl tar lsof
100
102
  curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
101
103
  ```
102
104
 
103
- Windows itself is not supported yet; use WSL. Other ways to install Claude Code: https://code.claude.com/docs/en/setup
105
+ On Windows, install Python 3.8+ from https://www.python.org/downloads/windows/ or the Microsoft Store, then in
106
+ PowerShell:
107
+
108
+ ```powershell
109
+ irm https://claude.ai/install.ps1 | iex # Claude Code, if you do not have it yet
110
+ python -m pip install --user pipx
111
+ python -m pipx ensurepath # then open a new terminal
112
+ ```
113
+
114
+ Other ways to install Claude Code: https://code.claude.com/docs/en/setup
104
115
 
105
116
  ## Installation
106
117
 
@@ -130,6 +141,49 @@ With Homebrew: `brew install jakoes-wu/tap/multi-claude` (upgrade with `brew upg
130
141
 
131
142
  Run `./install.sh --help` for all options.
132
143
 
144
+ ### Windows
145
+
146
+ Windows support is new. Install it with pipx; the installer script is for macOS and Linux only. If `pipx` is not
147
+ installed yet, run these in cmd or PowerShell (with Python 3.8 or later; use `python` instead of `py` if you do not
148
+ have the Python launcher):
149
+
150
+ ```
151
+ py -m pip install --user pipx
152
+ py -m pipx ensurepath
153
+ py -m pipx install multi-claude-cli
154
+ ```
155
+
156
+ Then open a new terminal so the `PATH` change takes effect, and check with `multi-claude --version`. With pipx
157
+ already on `PATH`, `pipx install multi-claude-cli` is enough. Everything above works the same way, with these
158
+ differences:
159
+
160
+ - Launchers are `claude-<name>.cmd` and `claude-auto.cmd` in `%USERPROFILE%\.local\bin` (the directory
161
+ `pipx ensurepath` adds to `PATH`), so they run from PowerShell and cmd alike. They read the proxy, variables
162
+ and arguments from the configuration each time they start, so a change takes effect without `apply`.
163
+ - Account directories are in `%USERPROFILE%\.cc` and the configuration in `%APPDATA%\multi-claude`.
164
+ - Shared directories are linked with directory junctions, which need no special rights. Sharing a single file
165
+ (such as `CLAUDE.md`) needs a file symbolic link, which needs Developer Mode (Settings > System > For
166
+ developers) or an administrator terminal; without it, `add` and `apply` stop and say so. Exclude the file
167
+ with `--shared-exclude` to share the rest.
168
+ - `migrate-default` refuses while any Claude process is running: Windows does not let it see which account
169
+ another process uses. Close every Claude window, terminal and IDE first.
170
+ - `statusline` needs Git for Windows, whose Git Bash is what Claude Code uses to run the status line. A
171
+ `statusLine` setting with `"shell": "powershell"` cannot be wrapped.
172
+ - Paths containing `"`, `%` or `!` cannot be written into a `.cmd` launcher; `apply` reports such an account as
173
+ a conflict.
174
+ - cmd reads a `.cmd` file in the console code page, so a launcher never contains non-ASCII characters. When the
175
+ Python or package path has them (for example a Chinese user name), the launcher writes it as
176
+ `%USERPROFILE%\...` (or `%LOCALAPPDATA%`, `%APPDATA%`), or else as its 8.3 short name. A non-ASCII path
177
+ that cannot be written with a variable and has no short name is reported as a conflict.
178
+ - When Claude exits (Ctrl+C or `/exit`), cmd may flash "Terminate batch job (Y/N)?" for a moment: the launcher is
179
+ a batch file. It closes by itself; Claude has already exited and nothing is lost.
180
+ - `multi-claude completion powershell | Add-Content $PROFILE` adds tab completion to PowerShell. Do not use `>>`:
181
+ Windows PowerShell 5.1 writes it as UTF-16 and breaks an existing profile. If the profile's directory does not
182
+ exist yet, create it first with `New-Item -ItemType Directory -Force (Split-Path $PROFILE)`.
183
+
184
+ Continuous integration runs the Windows-specific tests on Windows (`tests/test_windows.py`); the rest of the test
185
+ suite runs on macOS and Linux only.
186
+
133
187
  ## Quick start
134
188
 
135
189
  ### 1. Create one account per login
@@ -452,10 +506,13 @@ Notes:
452
506
 
453
507
  `multi-claude doctor` checks, without changing anything: the configuration and any unfinished migration, whether `claude` and the launcher directory are on `PATH`, whether each launcher is up to date and not hidden by another file of the same name, whether each account directory exists and is private (`0700`), the default account's link, broken shared links, whether a login exists, and variables such as `ANTHROPIC_API_KEY` that override the accounts' logins. Every problem comes with the command that fixes it. It exits with 1 when there is an error; `--json` prints the results for scripts.
454
508
 
509
+ `multi-claude doctor --network` also checks that each account can reach `api.anthropic.com` and `platform.claude.com` through the proxy it starts with (including the `env` of its `settings.json`). It connects step by step and says where it stopped: the proxy is not running, the proxy cannot get through, a name does not resolve, the secure connection was cut, the certificate could not be verified, a timeout, or `NO_PROXY` sends the account around its proxy. SOCKS and malformed proxy URLs, which Claude Code cannot use, are reported without connecting; https proxies are not checked. It sends one `HEAD /` per address and proxy (a second one if the first fails), with no login or token; addresses covered by `NO_PROXY` are checked directly.
510
+
455
511
  ```sh
456
512
  multi-claude completion bash > ~/.local/share/bash-completion/completions/multi-claude
457
513
  multi-claude completion zsh > "${fpath[1]}/_multi-claude" # or any directory on your fpath
458
514
  multi-claude completion fish > ~/.config/fish/completions/multi-claude.fish
515
+ multi-claude completion powershell | Add-Content $PROFILE # Windows PowerShell
459
516
  ```
460
517
 
461
518
  ## Migrating `~/.claude`
@@ -554,7 +611,7 @@ Every command's `--help` ends with examples you can copy, covering its options,
554
611
  | `multi-claude list [--verbose \| --json \| --names] [--no-refresh] [--offline]` | Show accounts, launchers, login state and the plan and billing channel looked up from Anthropic. `--json` adds usage and is meant for scripts; `--names` prints only the names; `--no-refresh` does not renew expired access tokens (never writes to a login); `--offline` skips the plan lookup (no token read, no network). A `RENEWS` column (and a `renews` key in `--json`) shows the renewal dates you recorded. |
555
612
  | `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
556
613
  | `multi-claude usage --history [NAME] [--days N] [--by day\|model] [--json]` | Token usage per day or model, counted from the session files on this machine (default: 7 days). |
557
- | `multi-claude doctor [--json] [--verbose]` | Check the configuration, launchers, account directories, shared links, logins and environment. |
614
+ | `multi-claude doctor [--json] [--verbose] [--network]` | Check the configuration, launchers, account directories, shared links, logins and environment; `--network` also checks each account's connection to Anthropic. |
558
615
  | `multi-claude route DIR NAME`, `route DIR --remove`, `route --default NAME`, `route --no-default` | Choose an account by directory for `claude-auto`. |
559
616
  | `multi-claude which [DIR]` | Show which account `claude-auto` would use in `DIR` (default: the current directory). |
560
617
  | `multi-claude rename OLD NEW` | Rename an account and its launcher. The directory and the login stay. |
@@ -577,13 +634,13 @@ Every write command accepts `--dry-run`, and prints only what changes; add `--ve
577
634
  | ---- | ---- |
578
635
  | Account directories | `~/.cc/<name>` |
579
636
  | Launchers | `~/.local/bin/claude-<name>` |
580
- | Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`) |
637
+ | Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`); on Windows `%APPDATA%\multi-claude\config.json` |
581
638
 
582
639
  Account names may contain letters, digits and `._@+-`, must start with a letter or digit, and are case-insensitive (`Work` and `work` are the same account). An email address works as a name. Entries starting with `.` in the root directory are never treated as accounts.
583
640
 
584
641
  ### Declarative setup with `apply`
585
642
 
586
- See [`examples/config.example.json`](examples/config.example.json):
643
+ See [`examples/config.example.json`](https://github.com/jakoes-wu/multi-claude/blob/main/examples/config.example.json):
587
644
 
588
645
  ```json
589
646
  {
@@ -644,7 +701,7 @@ This removes the tool only. Your configuration, account directories (including t
644
701
 
645
702
  ## Contributing
646
703
 
647
- See [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with:
704
+ See [CONTRIBUTING.md](https://github.com/jakoes-wu/multi-claude/blob/main/CONTRIBUTING.md). Run the tests with:
648
705
 
649
706
  ```sh
650
707
  python3 -m unittest discover -s tests -t tests
@@ -652,4 +709,4 @@ python3 -m unittest discover -s tests -t tests
652
709
 
653
710
  ## License
654
711
 
655
- [MIT](LICENSE)
712
+ [MIT](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
@@ -1,16 +1,17 @@
1
1
  # multi-claude
2
2
 
3
- **English** | [简体中文](README.zh-CN.md)
3
+ **English** | [简体中文](https://github.com/jakoes-wu/multi-claude/blob/main/README.zh-CN.md)
4
4
 
5
5
  [![Release](https://img.shields.io/github/v/release/jakoes-wu/multi-claude)](https://github.com/jakoes-wu/multi-claude/releases)
6
6
  [![CI](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml/badge.svg)](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml)
7
7
  [![PyPI](https://img.shields.io/pypi/v/multi-claude-cli)](https://pypi.org/project/multi-claude-cli/)
8
8
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
9
- [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
10
+ ![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)
10
11
 
11
12
  Use several [Claude Code](https://code.claude.com/docs) accounts on one machine, at the same time. Each account keeps its own login, settings and history and, if you like, its own proxy. No more logging out and in again.
12
13
 
13
- It never copies or exports your login and never edits your shell startup files. Only `list` reads an account's access token and goes online, to ask Anthropic for the current plan (renewing the token first if it has expired); `list --offline` skips all of that.
14
+ It never copies or exports your login and never edits your shell startup files. Only `list` reads an account's access token and goes online, to ask Anthropic for the current plan (renewing the token first if it has expired); `list --offline` skips all of that. `doctor --network` also goes online, but only when you ask for it and without any login.
14
15
 
15
16
  ```sh
16
17
  claude-work # Claude Code with your work account
@@ -38,7 +39,7 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
38
39
  ## Features
39
40
 
40
41
  - **Your login stays yours** — logins are never copied or exported. One command touches them: `list` takes each account's access token (from the keychain, or `.credentials.json` on Linux) to ask Anthropic which plan the account is on, and when that token has expired it renews it the way Claude Code does and saves the new one in the same place. Tokens stay in memory and go only to Anthropic (`api.anthropic.com`, and `platform.claude.com` for a renewal), through that account's proxy. `list --no-refresh` never writes, `list --offline` reads nothing. Every other command only checks whether a login exists. `migrate-default` and `restore` move whole account directories, and on Linux the credentials file moves with its directory without being opened.
41
- - **Only `list` goes online** — its plan lookup (one request per account, plus one to renew an expired token) is the only connection multi-claude makes: no telemetry, no usage reporting, no update checks. `list --offline` makes none. Only the installer, pipx or Homebrew download the package.
42
+ - **Only `list` and `doctor --network` go online** — the plan lookup of `list` (one request per account, plus one to renew an expired token) and the connection check of `doctor --network` (one `HEAD /` without credentials to `api.anthropic.com` and `platform.claude.com` per proxy, repeated once if it fails) are the only connections multi-claude makes: no telemetry, no usage reporting, no update checks. `list --offline` makes none. Only the installer, pipx or Homebrew download the package.
42
43
  - **Leaves your shell alone** — when `PATH` needs a change, it prints the line to add instead of editing `~/.zshrc` or `~/.bashrc`.
43
44
  - **Changes only what you ask** — it writes its own configuration and state, the launchers, account directories (including the VS Code data that `code` keeps in `<root>/.apps`) and shared links. A Claude Code settings file is changed only by `statusline install` (the file you name, with a backup) and `add --config-from` (the new account's `settings.json`).
44
45
  - **Migrate the default account** — move your existing `~/.claude` into the account root without logging out. `claude` keeps working exactly as before.
@@ -51,13 +52,13 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
51
52
 
52
53
  ## Prerequisites
53
54
 
54
- **Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups and token renewals of `list`, sent to Anthropic.
55
+ **Servers.** None. multi-claude runs only on your own computer; its only network requests are the plan lookups and token renewals of `list` and the connection check of `doctor --network`, all sent to Anthropic.
55
56
 
56
57
  **Your computer.**
57
58
 
58
59
  | | Needed | Optional |
59
60
  | ---- | ---- | ---- |
60
- | Operating system | macOS or Linux (WSL counts as Linux) | — |
61
+ | Operating system | macOS, Linux (WSL counts as Linux) or Windows | — |
61
62
  | Tools | Python 3.8+ (`python3`, standard library only); Claude Code (`claude`); `curl` or `wget`, and `tar`, for the installer | `pipx` or Homebrew, to install from PyPI or the tap; `lsof`, so that `migrate-default` and `restore` can find processes using `~/.claude` (Linux falls back to `/proc`); VS Code's `code` command, for `multi-claude code` |
62
63
 
63
64
  Install what is missing:
@@ -73,7 +74,16 @@ sudo apt install -y python3 curl tar lsof
73
74
  curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
74
75
  ```
75
76
 
76
- Windows itself is not supported yet; use WSL. Other ways to install Claude Code: https://code.claude.com/docs/en/setup
77
+ On Windows, install Python 3.8+ from https://www.python.org/downloads/windows/ or the Microsoft Store, then in
78
+ PowerShell:
79
+
80
+ ```powershell
81
+ irm https://claude.ai/install.ps1 | iex # Claude Code, if you do not have it yet
82
+ python -m pip install --user pipx
83
+ python -m pipx ensurepath # then open a new terminal
84
+ ```
85
+
86
+ Other ways to install Claude Code: https://code.claude.com/docs/en/setup
77
87
 
78
88
  ## Installation
79
89
 
@@ -103,6 +113,49 @@ With Homebrew: `brew install jakoes-wu/tap/multi-claude` (upgrade with `brew upg
103
113
 
104
114
  Run `./install.sh --help` for all options.
105
115
 
116
+ ### Windows
117
+
118
+ Windows support is new. Install it with pipx; the installer script is for macOS and Linux only. If `pipx` is not
119
+ installed yet, run these in cmd or PowerShell (with Python 3.8 or later; use `python` instead of `py` if you do not
120
+ have the Python launcher):
121
+
122
+ ```
123
+ py -m pip install --user pipx
124
+ py -m pipx ensurepath
125
+ py -m pipx install multi-claude-cli
126
+ ```
127
+
128
+ Then open a new terminal so the `PATH` change takes effect, and check with `multi-claude --version`. With pipx
129
+ already on `PATH`, `pipx install multi-claude-cli` is enough. Everything above works the same way, with these
130
+ differences:
131
+
132
+ - Launchers are `claude-<name>.cmd` and `claude-auto.cmd` in `%USERPROFILE%\.local\bin` (the directory
133
+ `pipx ensurepath` adds to `PATH`), so they run from PowerShell and cmd alike. They read the proxy, variables
134
+ and arguments from the configuration each time they start, so a change takes effect without `apply`.
135
+ - Account directories are in `%USERPROFILE%\.cc` and the configuration in `%APPDATA%\multi-claude`.
136
+ - Shared directories are linked with directory junctions, which need no special rights. Sharing a single file
137
+ (such as `CLAUDE.md`) needs a file symbolic link, which needs Developer Mode (Settings > System > For
138
+ developers) or an administrator terminal; without it, `add` and `apply` stop and say so. Exclude the file
139
+ with `--shared-exclude` to share the rest.
140
+ - `migrate-default` refuses while any Claude process is running: Windows does not let it see which account
141
+ another process uses. Close every Claude window, terminal and IDE first.
142
+ - `statusline` needs Git for Windows, whose Git Bash is what Claude Code uses to run the status line. A
143
+ `statusLine` setting with `"shell": "powershell"` cannot be wrapped.
144
+ - Paths containing `"`, `%` or `!` cannot be written into a `.cmd` launcher; `apply` reports such an account as
145
+ a conflict.
146
+ - cmd reads a `.cmd` file in the console code page, so a launcher never contains non-ASCII characters. When the
147
+ Python or package path has them (for example a Chinese user name), the launcher writes it as
148
+ `%USERPROFILE%\...` (or `%LOCALAPPDATA%`, `%APPDATA%`), or else as its 8.3 short name. A non-ASCII path
149
+ that cannot be written with a variable and has no short name is reported as a conflict.
150
+ - When Claude exits (Ctrl+C or `/exit`), cmd may flash "Terminate batch job (Y/N)?" for a moment: the launcher is
151
+ a batch file. It closes by itself; Claude has already exited and nothing is lost.
152
+ - `multi-claude completion powershell | Add-Content $PROFILE` adds tab completion to PowerShell. Do not use `>>`:
153
+ Windows PowerShell 5.1 writes it as UTF-16 and breaks an existing profile. If the profile's directory does not
154
+ exist yet, create it first with `New-Item -ItemType Directory -Force (Split-Path $PROFILE)`.
155
+
156
+ Continuous integration runs the Windows-specific tests on Windows (`tests/test_windows.py`); the rest of the test
157
+ suite runs on macOS and Linux only.
158
+
106
159
  ## Quick start
107
160
 
108
161
  ### 1. Create one account per login
@@ -425,10 +478,13 @@ Notes:
425
478
 
426
479
  `multi-claude doctor` checks, without changing anything: the configuration and any unfinished migration, whether `claude` and the launcher directory are on `PATH`, whether each launcher is up to date and not hidden by another file of the same name, whether each account directory exists and is private (`0700`), the default account's link, broken shared links, whether a login exists, and variables such as `ANTHROPIC_API_KEY` that override the accounts' logins. Every problem comes with the command that fixes it. It exits with 1 when there is an error; `--json` prints the results for scripts.
427
480
 
481
+ `multi-claude doctor --network` also checks that each account can reach `api.anthropic.com` and `platform.claude.com` through the proxy it starts with (including the `env` of its `settings.json`). It connects step by step and says where it stopped: the proxy is not running, the proxy cannot get through, a name does not resolve, the secure connection was cut, the certificate could not be verified, a timeout, or `NO_PROXY` sends the account around its proxy. SOCKS and malformed proxy URLs, which Claude Code cannot use, are reported without connecting; https proxies are not checked. It sends one `HEAD /` per address and proxy (a second one if the first fails), with no login or token; addresses covered by `NO_PROXY` are checked directly.
482
+
428
483
  ```sh
429
484
  multi-claude completion bash > ~/.local/share/bash-completion/completions/multi-claude
430
485
  multi-claude completion zsh > "${fpath[1]}/_multi-claude" # or any directory on your fpath
431
486
  multi-claude completion fish > ~/.config/fish/completions/multi-claude.fish
487
+ multi-claude completion powershell | Add-Content $PROFILE # Windows PowerShell
432
488
  ```
433
489
 
434
490
  ## Migrating `~/.claude`
@@ -527,7 +583,7 @@ Every command's `--help` ends with examples you can copy, covering its options,
527
583
  | `multi-claude list [--verbose \| --json \| --names] [--no-refresh] [--offline]` | Show accounts, launchers, login state and the plan and billing channel looked up from Anthropic. `--json` adds usage and is meant for scripts; `--names` prints only the names; `--no-refresh` does not renew expired access tokens (never writes to a login); `--offline` skips the plan lookup (no token read, no network). A `RENEWS` column (and a `renews` key in `--json`) shows the renewal dates you recorded. |
528
584
  | `multi-claude usage [NAME] [--json]` | Show the last known 5-hour and 7-day usage of each account. |
529
585
  | `multi-claude usage --history [NAME] [--days N] [--by day\|model] [--json]` | Token usage per day or model, counted from the session files on this machine (default: 7 days). |
530
- | `multi-claude doctor [--json] [--verbose]` | Check the configuration, launchers, account directories, shared links, logins and environment. |
586
+ | `multi-claude doctor [--json] [--verbose] [--network]` | Check the configuration, launchers, account directories, shared links, logins and environment; `--network` also checks each account's connection to Anthropic. |
531
587
  | `multi-claude route DIR NAME`, `route DIR --remove`, `route --default NAME`, `route --no-default` | Choose an account by directory for `claude-auto`. |
532
588
  | `multi-claude which [DIR]` | Show which account `claude-auto` would use in `DIR` (default: the current directory). |
533
589
  | `multi-claude rename OLD NEW` | Rename an account and its launcher. The directory and the login stay. |
@@ -550,13 +606,13 @@ Every write command accepts `--dry-run`, and prints only what changes; add `--ve
550
606
  | ---- | ---- |
551
607
  | Account directories | `~/.cc/<name>` |
552
608
  | Launchers | `~/.local/bin/claude-<name>` |
553
- | Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`) |
609
+ | Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`); on Windows `%APPDATA%\multi-claude\config.json` |
554
610
 
555
611
  Account names may contain letters, digits and `._@+-`, must start with a letter or digit, and are case-insensitive (`Work` and `work` are the same account). An email address works as a name. Entries starting with `.` in the root directory are never treated as accounts.
556
612
 
557
613
  ### Declarative setup with `apply`
558
614
 
559
- See [`examples/config.example.json`](examples/config.example.json):
615
+ See [`examples/config.example.json`](https://github.com/jakoes-wu/multi-claude/blob/main/examples/config.example.json):
560
616
 
561
617
  ```json
562
618
  {
@@ -617,7 +673,7 @@ This removes the tool only. Your configuration, account directories (including t
617
673
 
618
674
  ## Contributing
619
675
 
620
- See [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with:
676
+ See [CONTRIBUTING.md](https://github.com/jakoes-wu/multi-claude/blob/main/CONTRIBUTING.md). Run the tests with:
621
677
 
622
678
  ```sh
623
679
  python3 -m unittest discover -s tests -t tests
@@ -625,4 +681,4 @@ python3 -m unittest discover -s tests -t tests
625
681
 
626
682
  ## License
627
683
 
628
- [MIT](LICENSE)
684
+ [MIT](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
@@ -16,6 +16,7 @@ classifiers = [
16
16
  "Intended Audience :: Developers",
17
17
  "License :: OSI Approved :: MIT License",
18
18
  "Operating System :: MacOS",
19
+ "Operating System :: Microsoft :: Windows",
19
20
  "Operating System :: POSIX :: Linux",
20
21
  "Programming Language :: Python :: 3",
21
22
  "Programming Language :: Python :: 3.8",
@@ -1,3 +1,3 @@
1
1
  """multi-claude:管理多个 Claude Code 账号的配置目录、启动命令、代理与启动参数。"""
2
2
 
3
- __version__ = "0.14.0"
3
+ __version__ = "0.16.0"
@@ -7,6 +7,13 @@ if sys.argv[1:2] == ["statusline-hook"]:
7
7
  from .hook import main as hook_main
8
8
  sys.exit(hook_main(sys.argv[2:]))
9
9
 
10
+ from . import platform # noqa: E402
11
+
12
+ # 闸门放在导入 cli 之前:不支持的平台上 cli 依赖的模块可能根本加载不了,要先给出一句能看懂的报错
13
+ if not platform.is_supported():
14
+ sys.stderr.write("multi-claude: unsupported platform {}\n".format(sys.platform))
15
+ sys.exit(1)
16
+
10
17
  from .cli import main # noqa: E402
11
18
 
12
19
  sys.exit(main())
@@ -0,0 +1,43 @@
1
+ """Windows 启动命令(claude-<名称>.cmd、claude-auto.cmd)与状态栏后台进程的入口脚本。
2
+
3
+ 调用方式固定为 `"<python>" -I "<包目录>\\_entry.py" ...`(方案 feature-windows-support §5.1 D2):
4
+ - 用 `-I` 而不用 `-m`:`-m` 会把当前目录放进模块搜索路径,用户项目里的 json.py 之类文件会顶替标准库;
5
+ `-I` 也让脚本所在的包目录不进搜索路径,包里的 platform.py 等就不会顶替同名标准库模块。
6
+ - `-I` 同时忽略 PYTHONPATH,源码检出里运行时解释器找不到本包,所以这里把包的上一级目录追加到搜索路径末尾。
7
+
8
+ 本文件只能用标准库,且必须在导入本包之前完成搜索路径的设置。
9
+ """
10
+
11
+ import os
12
+ import runpy
13
+ import sys
14
+
15
+
16
+ def _same_dir(a: str, b: str) -> bool:
17
+ # 启动命令里的路径可能是 8.3 短名(MULTI_~1),而包从解释器原有的长路径加载:
18
+ # 只比 normcase 会把同一目录判成两个。realpath 在 Windows 上会展开短名与链接。
19
+ if os.path.normcase(os.path.realpath(a)) == os.path.normcase(os.path.realpath(b)):
20
+ return True
21
+ try:
22
+ return os.path.samefile(a, b)
23
+ except OSError:
24
+ return False
25
+
26
+
27
+ def _main() -> None:
28
+ package_dir = os.path.dirname(os.path.abspath(__file__))
29
+ parent = os.path.dirname(package_dir)
30
+ # 追加到末尾而不是插到最前:已安装的同名标准库模块仍优先于包目录之外的任何东西
31
+ if parent not in sys.path:
32
+ sys.path.append(parent)
33
+ import multi_claude
34
+ loaded = os.path.dirname(os.path.abspath(multi_claude.__file__))
35
+ if not _same_dir(loaded, package_dir):
36
+ # 解释器的 site-packages 里另装了一份 multi_claude:继续运行就是在用另一个版本
37
+ sys.stderr.write("multi-claude: loaded the package from {} instead of {}; reinstall multi-claude\n".format(
38
+ loaded, package_dir))
39
+ sys.exit(1)
40
+ runpy.run_module("multi_claude", run_name="__main__", alter_sys=True)
41
+
42
+
43
+ _main()
@@ -53,6 +53,9 @@ def default_dir() -> str:
53
53
 
54
54
 
55
55
  def render_launcher(config: Config, account: Account) -> str:
56
+ if platform.IS_WINDOWS:
57
+ python, entry = launcher.windows_command_paths()
58
+ return launcher.render_windows(account.name, account_dir(config, account.name), python, entry)
56
59
  return launcher.render(account.name, account.identity, account_dir(config, account.name), default_dir(),
57
60
  account.proxy, config.effective_env(account), config.effective_args(account))
58
61
 
@@ -169,6 +172,11 @@ def _plan_router(old: Config, new: Config) -> List[Action]:
169
172
  actions.append(Action(CONFLICT, "router", new_path, "claude-auto is the directory router; an account "
170
173
  "named 'auto' cannot coexist with routes"))
171
174
 
175
+ if platform.IS_WINDOWS:
176
+ unsafe = launcher.cmd_unsafe_reason([], launcher.windows_command_paths())
177
+ if unsafe is not None:
178
+ actions.append(Action(CONFLICT, "router", new_path, unsafe))
179
+ return actions
172
180
  content = routes.render_router(new)
173
181
  kind = entry_kind(new_path)
174
182
  if kind == KIND_MISSING:
@@ -230,12 +238,18 @@ def _make_private_dir(directory: str) -> None:
230
238
  if not os.path.isdir(parent):
231
239
  os.makedirs(parent, mode=0o700)
232
240
  os.mkdir(directory, 0o700)
233
- os.chmod(directory, 0o700)
241
+ # Windows 上 chmod 只能改只读属性,访问控制靠用户目录的 ACL
242
+ if not platform.IS_WINDOWS:
243
+ os.chmod(directory, 0o700)
234
244
 
235
245
 
236
246
  def _plan_launcher(bin_dir: str, config: Config, account: Account) -> List[Action]:
237
247
  name = account.name
238
248
  path = launcher.launcher_path(bin_dir, name)
249
+ if platform.IS_WINDOWS:
250
+ unsafe = launcher.cmd_unsafe_reason([account_dir(config, name)], launcher.windows_command_paths())
251
+ if unsafe is not None:
252
+ return [Action(CONFLICT, "launcher", path, unsafe)]
239
253
  content = render_launcher(config, account)
240
254
  kind = entry_kind(path)
241
255
  if kind == KIND_MISSING:
@@ -296,6 +310,12 @@ def execute(old: Config, new: Config, actions: List[Action], *, dry_run: bool, v
296
310
  return EXIT_CONFLICT
297
311
  if dry_run:
298
312
  return EXIT_OK
313
+ # Windows 上没有权限建文件链接时,在写配置之前就停下,免得配置已写、链接只建了一半(方案 C4)
314
+ file_links = shared.file_links_needed(actions) if platform.IS_WINDOWS else []
315
+ if file_links and not platform.can_create_file_link():
316
+ error(shared.file_link_message("the file links " + ", ".join(file_links),
317
+ sorted({os.path.basename(path) for path in file_links})), phase="shared-link")
318
+ return EXIT_ERROR
299
319
  config_action = actions[0]
300
320
  if config_action.changes:
301
321
  try:
@@ -10,7 +10,6 @@ assistant 记录,结果写一行带 `tool_result` 块的 user 记录,发布
10
10
  这段字节的 SHA-256),查询时据此把副本里交接前的记录归回来源账号(§5.1.3)。
11
11
  """
12
12
 
13
- import fcntl
14
13
  import hashlib
15
14
  import json
16
15
  import os
@@ -244,13 +243,13 @@ def record_handoff(handoff: "sessions.Plan", source_account_dir: str, target_acc
244
243
  data = (json.dumps(entry, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8")
245
244
  path = log_path()
246
245
  os.makedirs(os.path.dirname(path), exist_ok=True)
247
- fd = os.open(path, os.O_RDWR | os.O_APPEND | os.O_CREAT, 0o600)
246
+ fd = os.open(path, os.O_RDWR | os.O_APPEND | os.O_CREAT | getattr(os, "O_BINARY", 0), 0o600)
248
247
  try:
249
248
  # 阻塞等待:两个 handoff 同时追加时排队,不交错。锁的是本文件,不是工具的写锁(handoff 不拿写锁)
250
- fcntl.flock(fd, fcntl.LOCK_EX)
249
+ platform.lock_fd(fd, blocking=True)
251
250
  # 只写描述符读不了末字节,所以上面用 O_RDWR 打开
252
251
  size = os.fstat(fd).st_size
253
- if size and os.pread(fd, 1, size - 1) != b"\n":
252
+ if size and platform.read_at(fd, 1, size - 1) != b"\n":
254
253
  data = b"\n" + data
255
254
  view = memoryview(data)
256
255
  while view:
@@ -258,7 +257,7 @@ def record_handoff(handoff: "sessions.Plan", source_account_dir: str, target_acc
258
257
  view = view[written:]
259
258
  os.fsync(fd)
260
259
  finally:
261
- # 关闭即释放 flock
260
+ # 关闭即释放锁(flock 与 msvcrt 字节锁都如此)
262
261
  os.close(fd)
263
262
 
264
263
 
@@ -22,7 +22,7 @@ import time
22
22
  from datetime import datetime, timezone
23
23
  from typing import Dict, List, Optional, Tuple
24
24
 
25
- from . import __version__, accounts, usage
25
+ from . import __version__, accounts, platform, usage
26
26
  from .actions import error, info, warn
27
27
  from .config import Account, Config
28
28
  from .fsutil import KIND_DIR, KIND_FILE, KIND_LINK, KIND_MISSING, atomic_write, entry_kind
@@ -114,10 +114,12 @@ def _create_temp(parent: str):
114
114
  for _attempt in range(100):
115
115
  path = os.path.join(parent, ".multi-claude-export.{}.tar.gz".format(os.urandom(6).hex()))
116
116
  try:
117
- fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
117
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o600)
118
118
  except FileExistsError:
119
119
  continue
120
- os.fchmod(fd, 0o600) # 不受 umask 影响:包里可能有密钥,只给本人读
120
+ # 不受 umask 影响:包里可能有密钥,只给本人读。Windows 没有 fchmod,权限由用户目录的 ACL 保证
121
+ if hasattr(os, "fchmod"):
122
+ os.fchmod(fd, 0o600)
121
123
  return path, fd
122
124
  raise OSError("cannot create a temporary file in {}".format(parent))
123
125
 
@@ -132,7 +134,7 @@ def _publish(tmp_path: str, out_path: str) -> None:
132
134
  # 不支持硬链接的文件系统(如 exFAT):再检查一次后改名
133
135
  if os.path.lexists(out_path):
134
136
  raise OSError("{} appeared while exporting; nothing was written".format(out_path))
135
- os.replace(tmp_path, out_path)
137
+ platform.retry_on_sharing_violation(os.replace, tmp_path, out_path)
136
138
  return
137
139
  os.unlink(tmp_path)
138
140
 
@@ -350,7 +352,9 @@ def _check_item_types(members: List[tarfile.TarInfo]) -> None:
350
352
  def _check_member(member: tarfile.TarInfo) -> None:
351
353
  name = member.name
352
354
  parts = name.split("/")
353
- if name.startswith("/") or "\\" in name or "\x00" in name or any(part in ("", ".", "..") for part in parts):
355
+ # Windows 上成员名里的冒号会被解析成盘符(C:x)或 NTFS 备用数据流(a:b),一律拒绝;POSIX 上冒号是普通字符
356
+ if (name.startswith("/") or "\\" in name or "\x00" in name or (platform.IS_WINDOWS and ":" in name)
357
+ or any(part in ("", ".", "..") for part in parts)):
354
358
  raise BundleError("unsafe path in bundle: {!r}".format(name))
355
359
  if not (member.isfile() or member.isdir()):
356
360
  raise BundleError("unsupported entry in bundle (only files and directories): {!r}".format(name))
@@ -453,9 +457,9 @@ def _apply(plan: _Plan, directory: str, staging: str, state_path: str) -> None:
453
457
  target = os.path.join(directory, item)
454
458
  if action == "replace":
455
459
  backup = _backup_name(target, stamp)
456
- os.rename(target, backup)
460
+ platform.retry_on_sharing_violation(os.rename, target, backup)
457
461
  info("backup: {}".format(backup))
458
- os.replace(os.path.join(staging, ITEMS_DIR, item), target)
462
+ platform.retry_on_sharing_violation(os.replace, os.path.join(staging, ITEMS_DIR, item), target)
459
463
  if data is None:
460
464
  return
461
465
  warn("Claude sessions of this account that are running keep their own copy of {} and may overwrite "