feat(config): interprocess write lock — eliminate watchdog TOCTOU race - #57
Merged
Conversation
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-assignment |
2 |
unresolved-import |
1 |
First entries
hermes_cli/config.py:39: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `<module 'msvcrt'>`
hermes_cli/config.py:34: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `<module 'fcntl'>`
tests/hermes_cli/test_config_lock.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
✅ Fixed issues: none
Unchanged: 4570 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
dizhaky
force-pushed
the
claude/config-write-lock
branch
2 times, most recently
from
June 29, 2026 21:05
2e1fe07 to
1b719fc
Compare
- Add config_write_lock() exclusive OS file lock (fcntl.flock / msvcrt) - Add config_read_lock() shared lock for external readers (e.g. watchdog) - Extract _write_config_to_disk() to avoid re-entrant lock deadlock - Wrap save_config() write+seal in exclusive lock — zero race window - Update verify_config_integrity(locked=False) with optional locked kwarg - Add restore_config() safe helper: lock → backup → save → reseal - Tests: 17 cases covering lock lifecycle, integrity, and restore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MnmdLAbTdbTo66Uc2tK31x
…ports Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MnmdLAbTdbTo66Uc2tK31x
dizhaky
force-pushed
the
claude/config-write-lock
branch
from
June 29, 2026 21:13
1b719fc to
8d8c6db
Compare
dizhaky
marked this pull request as ready for review
June 29, 2026 21:13
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This was referenced Jul 2, 2026
dizhaky
added a commit
that referenced
this pull request
Jul 7, 2026
…writes (#82) save_config()/restore_config() already keep the local .sha256 sidecar in sync (PR #57), but the *external* git-backed baseline used by `hermes config verify` (PR #67) was only ever updated by an explicit `hermes config seal`. Any authorized write through save_config() — model scanner, /model command, platform setup flows via write_platform_config_field() — desynced that baseline, so the Config Integrity Watchdog cron job flagged the legitimate change as tampering: "Hermes Config Integrity Failure! The configuration hash does not match the sealed baseline." This is the same scanner/watchdog TOCTOU conflict documented in docs/plans/2026-07-02-scanner-watchdog-conflict-resolution.md, just recurring one layer over in the newer git-backed mechanism. _write_config_to_disk() now also calls the config-integrity-watchdog skill's seal() (quietly) whenever $HERMES_DOTFILES_DIR is configured, keeping the git-backed log current on every authorized write. Extracted _find_core_module() (returns None instead of exiting) so this can opportunistically no-op on machines without the watchdog set up. restore_quick_snapshot() in backup.py gets the same treatment for config.yaml restores from a quick snapshot. Co-authored-by: Test <test@test.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
save_config()writesconfig.yamlthen separately writes the SHA256 seal inseal_config(). If the Config Integrity Watchdog fires in that gap (milliseconds), it seescurrent != sealed, declares tampering, and triggers a false restore — resurrecting the scanner/watchdog conflict fixed in #52._CONFIG_LOCK(a threading lock) protects same-process concurrent writes but does nothing for the watchdog, which runs in a separate cron process.Slack thread: https://mfc-nyc.slack.com/archives/C0BD8QBUSJF/p1782743855322599
Solution
An OS-level file lock (
fcntl.flockon POSIX,msvcrt.lockingon Windows) thatsave_config()holds exclusively from beforeatomic_yaml_writethrough theseal_config()write. No external process can observe an intermediate state.Changes
hermes_cli/config.pyget_config_lock_path()~/.hermes/config.yaml.lockconfig_write_lock()config_read_lock()_write_config_to_disk()save_config()to avoidfcntl.flockre-entrancy deadlock whenrestore_config()calls intosave_config()save_config()config_write_lock()around write + seal — zero race windowverify_config_integrity(locked=False)lockedkwarg: whenTrue, acquiresconfig_read_lock()before reading, guaranteeing consistent staterestore_config(content, *, reason="")restore_deepseek_config.pytests/hermes_cli/test_config_lock.py17 new tests across 5 classes: lock paths, write lock lifecycle, read lock (including concurrent readers),
verify_config_integrity(tamper detection, locked mode, seal update on save), andrestore_config(dict/YAML string/reason in backup name/seal update after restore).Implementation note
fcntl.flock(LOCK_EX)is not reentrant on POSIX — re-acquiring an exclusive lock from the same process deadlocks.restore_config()holding the OS lock and callingsave_config()(which would re-acquire it) deadlocked in initial testing. Fixed by extracting_write_config_to_disk()as a private helper:save_config()callsconfig_write_lock()then_write_config_to_disk();restore_config()acquires both_CONFIG_LOCKandconfig_write_lock()and calls_write_config_to_disk()directly.Generated by Claude Code