Bug Description
The dashboard-specific Anthropic OAuth helper writes ~/.hermes/.anthropic_oauth.json directly with Path.write_text(...).
On a normal umask 022 system, that produces a 0644 credential file. The helper also skips the temp-file + os.replace() pattern already used by adjacent auth writers, so an interrupted write can leave the file in a weaker state than the rest of Hermes auth storage.
This is not a request for a new security boundary. It is a consistency bug in one dashboard-only credential write path.
Affected Component
hermes_cli/web_server.py
- helper:
_save_anthropic_oauth_creds()
Steps to Reproduce
- Check out current
main.
- Run a focused test like the one below under a standard
umask 022 shell.
- Call
_save_anthropic_oauth_creds(...) with _HERMES_OAUTH_FILE redirected to a temporary path.
- Inspect the resulting file mode.
Minimal reproduction logic:
old_umask = os.umask(0o022)
try:
_save_anthropic_oauth_creds('access-token', 'refresh-token', 123456)
finally:
os.umask(old_umask)
mode = oauth_file.stat().st_mode & 0o777
assert mode == 0o600
Expected Behavior
The dashboard helper should match Hermes's existing auth-storage conventions:
- owner-only file permissions (
0600)
- temp-file write + atomic
os.replace()
- cleanup of temporary files on failure
Actual Behavior
On current main, the helper:
- writes the final file directly
- relies on the process umask for permissions
- does not use
os.replace()
In local verification on fresh upstream main, the resulting file mode was 0644 under umask 022.
Root Cause Analysis
Other Hermes auth writers already use stricter semantics:
hermes_cli/auth.py::_save_auth_store() writes through a temp file and applies owner-only permissions
hermes_cli/auth.py::_save_qwen_cli_tokens() writes to a temp file and replaces it
hermes_cli/auth.py refresh writes also chmod the final file to 0600
But hermes_cli/web_server.py::_save_anthropic_oauth_creds() currently does:
_HERMES_OAUTH_FILE.parent.mkdir(parents=True, exist_ok=True)
_HERMES_OAUTH_FILE.write_text(json.dumps(payload, indent=2), encoding="utf-8")
So the dashboard flow is weaker than the rest of the repo's credential-write paths.
Proposed Fix
Keep the scope narrow:
- write the dashboard OAuth payload to a temp file
flush() + fsync() before replace
os.replace() into the final path
chmod(0600) on the final file
- remove temp files in a
finally block
Validation
I reproduced this locally on fresh upstream main with a focused regression test.
I also prepared a small PR with:
- the narrow helper fix
- a regression test for owner-only permissions under
umask 022
- a regression test that asserts atomic replace behavior is used
Tracked in PR #11004.
Bug Description
The dashboard-specific Anthropic OAuth helper writes
~/.hermes/.anthropic_oauth.jsondirectly withPath.write_text(...).On a normal
umask 022system, that produces a0644credential file. The helper also skips the temp-file +os.replace()pattern already used by adjacent auth writers, so an interrupted write can leave the file in a weaker state than the rest of Hermes auth storage.This is not a request for a new security boundary. It is a consistency bug in one dashboard-only credential write path.
Affected Component
hermes_cli/web_server.py_save_anthropic_oauth_creds()Steps to Reproduce
main.umask 022shell._save_anthropic_oauth_creds(...)with_HERMES_OAUTH_FILEredirected to a temporary path.Minimal reproduction logic:
Expected Behavior
The dashboard helper should match Hermes's existing auth-storage conventions:
0600)os.replace()Actual Behavior
On current
main, the helper:os.replace()In local verification on fresh upstream
main, the resulting file mode was0644underumask 022.Root Cause Analysis
Other Hermes auth writers already use stricter semantics:
hermes_cli/auth.py::_save_auth_store()writes through a temp file and applies owner-only permissionshermes_cli/auth.py::_save_qwen_cli_tokens()writes to a temp file and replaces ithermes_cli/auth.pyrefresh writes also chmod the final file to0600But
hermes_cli/web_server.py::_save_anthropic_oauth_creds()currently does:So the dashboard flow is weaker than the rest of the repo's credential-write paths.
Proposed Fix
Keep the scope narrow:
flush()+fsync()before replaceos.replace()into the final pathchmod(0600)on the final filefinallyblockValidation
I reproduced this locally on fresh upstream
mainwith a focused regression test.I also prepared a small PR with:
umask 022Tracked in PR #11004.