Windows: os.fchmod in credential write-back bricks the OAuth refresh token
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- python
- Domain
- authentication
Research direction
Start in anthropic/lib/credentials/_providers.py at _atomic_write_credentials (line 419), then trace its caller _call_user_oauth. Run the supplied no-credentials Windows reproduction and verify that credential write-back completes without the AttributeError and that the refreshed token is persisted.
Written by the indexing model from the issue text.
Description
SDK: anthropic 0.116.0 · Python: 3.10.11 · OS: Windows 11 (26200)
Summary
CredentialsFile._atomic_write_credentials calls os.fchmod(fd, 0o600) as the first statement of its write (anthropic/lib/credentials/_providers.py:419). os.fchmod is POSIX-only and does not exist on Windows, so every credential write-back raises AttributeError before any bytes are written.
On its own that would be a portability bug. What makes it destructive is the ordering in _call_user_oauth: the refresh_token grant is POSTed and succeeds first — the server rotates the refresh token and returns a new pair — and only then does the write-back crash and discard it. The token left on disk is the one the server has just invalidated.
The practical result on Windows is that any user_oauth profile is permanently bricked by its own first token refresh. Every subsequent call fails with 400 invalid_grant: Refresh token not found or invalid, and the only recovery is an interactive ant auth login — which buys roughly one access-token lifetime (~8h) before the same thing happens again. From the user's side this presents as "I have to sign in again every day", with no indication that the SDK is the cause.
Note that the directory-fsync twenty lines below the offending call is guarded, with the comment "Windows and some POSIX flavours don't support directory fds". The fchmod above it is not.
Reproduction (no credentials required)
import json, os, pathlib, tempfile
sandbox = pathlib.Path(tempfile.mkdtemp())
os.environ["ANTHROPIC_CONFIG_DIR"] = str(sandbox)
(sandbox / "configs").mkdir()
(sandbox / "credentials").mkdir()
(sandbox / "configs" / "default.json").write_text(json.dumps(
{"version": "1.0", "authentication": {"type": "user_oauth", "client_id": "x"}}))
creds = sandbox / "credentials" / "default.json"
creds.write_text(json.dumps({"version": "1.0", "refresh_token": "x"}))
from anthropic.lib.credentials import CredentialsFile
p = CredentialsFile()
p._config = json.loads((sandbox / "configs" / "default.json").read_text())
p._credentials_path = creds
p._atomic_write_credentials({"refresh_token": "new"})
print("wrote OK:", json.loads(creds.read_text()))
Actual, on Windows:
File "...\anthropic\lib\credentials\_providers.py", line 419, in _atomic_write_credentials
os.fchmod(fd, 0o600)
AttributeError: module 'os' has no attribute 'fchmod'. Did you mean: 'chmod'?
Expected: wrote OK: {'refresh_token': 'new'}.
Reproduction of the destructive behaviour (real profile)
ant auth login— note the mtime of%APPDATA%\Anthropic\credentials\default.json.- Force one refresh:
CredentialsFile()(force_refresh=True)→ raises theAttributeErrorabove. - The credentials file mtime has not changed.
- POST the still-stored
refresh_tokento/v1/oauth/token→400 invalid_grant: Refresh token not found or invalid.
Observed here: a credential minted at 09:14:13 was dead by 09:15, from a single refresh, with one consumer and no concurrency.
Suggested fix
Guard the call the same way the directory-fsync below it is guarded:
if hasattr(os, "fchmod"):
os.fchmod(fd, 0o600)
A no-op is the correct behaviour on Windows — the call exists to keep the credentials file at 0600, and POSIX permission bits have no meaning on NTFS. If restricting access on Windows is wanted, that needs an ACL (e.g. icacls), not a mode.
Two adjacent points, take or leave:
- Consider persisting before the old token is spent, or treating write-back failure as fatal to the refresh. Any exception between the successful POST and the completed write silently destroys the credential. Today that is
fchmod; tomorrow it could be a full disk or a locked file. _atomic_write_credentialshas no read-modify-write lock.mkstemp+os.replaceis atomic per writer, but two processes can each load refresh token A, both present it, and the loser is poisoned permanently. The docstring at:412anticipates concurrent writers and calls last-write-wins "fine for a best-effort cache" — which holds for a cache, but a rotating single-use refresh token is not one. Not what bit us, but reachable.
Workaround for other Windows users
Install a no-op before constructing any client:
import os
if not hasattr(os, "fchmod"):
os.fchmod = lambda fd, mode: None
Verified: with this in place, two consecutive forced refreshes both rotate and persist, the file mtime advances, and inference succeeds on the surviving credential.
- Dominant language
- Python
- Stars
- 3.9k
- Forks
- 857
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 8
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from anthropics/anthropic-sdk-python
-
Difficulty 1/5 Under an hour Newbie friendliness 92/100
anthropics/anthropic-sdk-python#1915 · 3 comments ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
anthropics/anthropic-sdk-python#1907 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
anthropics/anthropic-sdk-python#1676 · 1 comment ·
-
[Bug] append_messages() inside tool runner loop causes infinite loop (advanced usage docs example) Open
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
anthropics/anthropic-sdk-python#1536 · 4 comments ·
-
sdk
Difficulty 1/5 1-3 hours Newbie friendliness 72/100
anthropics/anthropic-sdk-python#1290 · 2 comments ·
All issues in anthropics/anthropic-sdk-python
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100