Problem
Under concurrent multi-user load (gateway with many active sessions), multiple processes/threads within the same profile can write to skill files simultaneously with no cross-process synchronization:
| Writer |
Path |
Scenario |
skill_manage tool |
tools/skill_manager_tool.py |
Agent-initiated or background review daemon |
| Curator |
agent/curator.py |
Auto-archive/delete stale skills |
| CLI |
hermes skills install/update/uninstall |
Manual operations |
| Background review |
daemon thread → forked AIAgent → skill_manage |
Triggers every ~10 turns per session; multiple sessions may fire concurrently |
None of these paths hold any cross-process lock. A background review in session A writing skill_manage(action='patch', ...) races with curator archiving the same skill in another thread, or two background reviews from different sessions patching the same SKILL.md simultaneously.
Additionally, the terminal tool's cwd state is shared across sessions (tools/terminal_tool.py line 1076: "the terminal env is shared across sessions").
Proposed Solution: Per-Skill File Lock
Use fcntl.flock() on the skill directory FD (not a separate lock file), scoped per skill name so writes to different skills are fully concurrent:
write "python-tips" → flock(skills/python-tips/) ← only this skill locked
write "git-workflow" → flock(skills/git-workflow/) ← no wait for python-tips
create new skill → flock(skills/) ← parent-dir lock to prevent
duplicate names
Why directory FD instead of a lock file
- No extra file to create / clean up / manage
fcntl.flock() on a directory fd works identically to file fd on Linux/macOS
- Kernel-managed: released on process death, no stale lock problem
- The directory is the natural serialization boundary — one skill = one dir
API
# agent/skill_lock.py
from contextlib import contextmanager
@contextmanager
def skill_write_lock(skill_name: str, timeout: float = 30):
"""Acquire a per-skill write lock, cross-process safe."""
skill_dir = Path(get_hermes_home()) / "skills" / skill_name
target = skill_dir if skill_dir.is_dir() else skill_dir.parent
fd = os.open(str(target), os.O_RDONLY)
try:
_flock_wait(fd, timeout)
yield
finally:
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
Integration points (3 files)
tools/skill_manager_tool.py — wrap all 6 actions (create/edit/patch/delete/write_file/remove_file)
agent/curator.py — wrap archive/delete operations
hermes_cli/skills.py — wrap install/update/uninstall
Delete handling
Before deleting a skill directory, os.rename to a temp location first, then shutil.rmtree — releases lock before slow deletion.
Why reads don't need the lock
Skill writes already use atomic write temp + rename internally. Readers always see either the old file or the new file, never a partial write. Curator moves are handled by the delete rename pattern above.
Windows fallback
Use msvcrt.locking() + PID-based lock file with stale detection.
Scope
- New file:
agent/skill_lock.py (~60 lines)
- Modified: 3 existing files
- Tests:
tests/agent/test_skill_lock.py
Notes
The same profile-level sharing problem also applies to terminal cwd state — tracked separately in tools/terminal_tool.py's ongoing "cwd rearchitecture" (step 1 in progress). This issue focuses on skills only.
If maintainers are interested, I'm willing to submit a PR implementing this plan.
Problem
Under concurrent multi-user load (gateway with many active sessions), multiple processes/threads within the same profile can write to skill files simultaneously with no cross-process synchronization:
skill_managetooltools/skill_manager_tool.pyagent/curator.pyhermes skills install/update/uninstallskill_manageNone of these paths hold any cross-process lock. A background review in session A writing
skill_manage(action='patch', ...)races with curator archiving the same skill in another thread, or two background reviews from different sessions patching the same SKILL.md simultaneously.Additionally, the
terminaltool's cwd state is shared across sessions (tools/terminal_tool.pyline 1076: "the terminal env is shared across sessions").Proposed Solution: Per-Skill File Lock
Use
fcntl.flock()on the skill directory FD (not a separate lock file), scoped per skill name so writes to different skills are fully concurrent:Why directory FD instead of a lock file
fcntl.flock()on a directory fd works identically to file fd on Linux/macOSAPI
Integration points (3 files)
tools/skill_manager_tool.py— wrap all 6 actions (create/edit/patch/delete/write_file/remove_file)agent/curator.py— wrap archive/delete operationshermes_cli/skills.py— wrap install/update/uninstallDelete handling
Before deleting a skill directory,
os.renameto a temp location first, thenshutil.rmtree— releases lock before slow deletion.Why reads don't need the lock
Skill writes already use atomic
write temp + renameinternally. Readers always see either the old file or the new file, never a partial write. Curator moves are handled by the delete rename pattern above.Windows fallback
Use
msvcrt.locking()+ PID-based lock file with stale detection.Scope
agent/skill_lock.py(~60 lines)tests/agent/test_skill_lock.pyNotes
The same profile-level sharing problem also applies to terminal cwd state — tracked separately in
tools/terminal_tool.py's ongoing "cwd rearchitecture" (step 1 in progress). This issue focuses on skills only.If maintainers are interested, I'm willing to submit a PR implementing this plan.