Native Windows (with Git for Windows installed) can now run the Hermes CLI and gateway end-to-end without crashing. install.ps1 already existed and the Git Bash terminal backend was already wired up — this PR fills the remaining gaps discovered by auditing every Windows-unsafe primitive (`signal.SIGKILL`, `os.kill(pid, 0)` probes, bare `fcntl`/`termios` imports) and by comparing hermes against how Claude Code, OpenCode, Codex, and Cline handle native Windows. ## What changed ### UTF-8 stdio (new module) - `hermes_cli/stdio.py` — single `configure_windows_stdio()` entry point. Flips the console code page to CP_UTF8 (65001), reconfigures `sys.stdout`/`stderr`/`stdin` to UTF-8, sets `PYTHONIOENCODING` + `PYTHONUTF8` for subprocesses. No-op on non-Windows. Opt out via `HERMES_DISABLE_WINDOWS_UTF8=1`. - Called early in `cli.py::main`, `hermes_cli/main.py::main`, and `gateway/run.py::main` so Unicode banners (box-drawing, geometric symbols, non-Latin chat text) don't `UnicodeEncodeError` on cp1252 consoles. ### Crash sites fixed - `hermes_cli/main.py:7970` (hermes update → stuck gateway sweep): raw `os.kill(pid, _signal.SIGKILL)` → `gateway.status.terminate_pid(pid, force=True)` which routes through `taskkill /T /F` on Windows. - `hermes_cli/profiles.py::_stop_gateway_process`: same fix — also converted SIGTERM path to `terminate_pid()` and widened OSError catch on the intermediate `os.kill(pid, 0)` probe. - `hermes_cli/kanban_db.py:2914, 3041`: raw `signal.SIGKILL` → `getattr(signal, "SIGKILL", signal.SIGTERM)` fallback (matches the pattern already used in `gateway/status.py`). ### OSError widening on `os.kill(pid, 0)` probes Windows raises `OSError` (WinError 87) for a gone PID instead of `ProcessLookupError`. Widened the catch at: - `gateway/run.py:15101` (`--replace` wait-for-exit loop — without this, the loop busy-spins the full 10s every Windows gateway start) - `hermes_cli/gateway.py:228, 460, 940` - `hermes_cli/profiles.py:777` - `tools/process_registry.py::_is_host_pid_alive` - `tools/browser_tool.py:1170, 1206` ### Dashboard PTY graceful degradation `hermes_cli/pty_bridge.py` depends on `fcntl`/`termios`/`ptyprocess`, none of which exist on native Windows. Previously a Windows dashboard would crash on `import hermes_cli.web_server` because of a top-level import. Now: - `hermes_cli/web_server.py` wraps the pty_bridge import in `try/except ImportError` and sets `_PTY_BRIDGE_AVAILABLE=False`. - The `/api/pty` WebSocket handler returns a friendly "use WSL2 for this tab" message instead of exploding. - Every other dashboard feature (sessions, jobs, metrics, config editor) runs natively on Windows. ### Dependency - `pyproject.toml`: add `tzdata>=2023.3; sys_platform == 'win32'` so Python's `zoneinfo` works on Windows (which has no IANA tzdata shipped with the OS). Credits @sprmn24 (PR #13182). ### Docs - README.md: removed "Native Windows is not supported"; added PowerShell one-liner and Git-for-Windows prerequisite note. - `website/docs/getting-started/installation.md`: new Windows section with capability matrix (everything native except the dashboard `/chat` PTY tab, which is WSL2-only). - `website/docs/user-guide/windows-wsl-quickstart.md`: reframed as "WSL2 as an alternative to native" rather than "the only way". - `website/docs/developer-guide/contributing.md`: updated cross-platform guidance with the `signal.SIGKILL` / `OSError` rules we enforce now. - `website/docs/user-guide/features/web-dashboard.md`: acknowledged native Windows works for everything except the embedded PTY pane. ## Why this shape Pulled from a survey of how other agent codebases handle native Windows (Claude Code, OpenCode, Codex, Cline): - All four treat Git Bash as the canonical shell on Windows, same as hermes already does in `tools/environments/local.py::_find_bash()`. - None of them force `SetConsoleOutputCP` — but they don't have to, Node/Rust write UTF-16 to the Win32 console API. Python does not get that for free, so we flip CP_UTF8 via ctypes. - None of them ship PowerShell-as-primary-shell (Claude Code exposes PS as a secondary tool; scope creep for this PR). - All of them use `taskkill /T /F` for force-kill on Windows, which is exactly what `gateway.status.terminate_pid(force=True)` does. ## Non-goals (deliberate scope limits) - No PowerShell-as-a-second-shell tool — worth designing separately. - No terminal routing rewrite (#12317, #15461, #19800 cluster) — that's the hardest design call and needs a separate doc. - No wholesale `open()` → `open(..., encoding="utf-8")` sweep (Tianworld cluster) — will do as follow-up if users hit actual breakage; most modern code already specifies it. ## Validation - 28 new tests in `tests/tools/test_windows_native_support.py` — all platform-mocked, pass on Linux CI. Cover: - `configure_windows_stdio` idempotency, opt-out, env-preservation - `terminate_pid` taskkill routing, failure → OSError, FileNotFoundError fallback - `getattr(signal, "SIGKILL", …)` fallback shape - `_is_host_pid_alive` OSError widening (Windows-gone-PID behavior) - Source-level checks that all entry points call `configure_windows_stdio` - pty_bridge import-guard present in `web_server.py` - README no longer says "not supported" - 12 pre-existing tests in `tests/tools/test_windows_compat.py` still pass. - `tests/hermes_cli/` ran fully (3909 passed, 9 failures — all confirmed pre-existing on main by stash-test). - `tests/gateway/` ran fully (5021 passed, 1 pre-existing failure). - `tests/tools/test_process_registry.py` + `test_browser_*` pass. - Manual smoke: `import hermes_cli.stdio; import gateway.run; import hermes_cli.web_server` — all clean, `_PTY_BRIDGE_AVAILABLE=True` on Linux (as expected). ## Files - New: `hermes_cli/stdio.py`, `tests/tools/test_windows_native_support.py` - Modified: `cli.py`, `gateway/run.py`, `hermes_cli/main.py`, `hermes_cli/profiles.py`, `hermes_cli/gateway.py`, `hermes_cli/kanban_db.py`, `hermes_cli/pty_bridge.py`, `hermes_cli/web_server.py`, `tools/browser_tool.py`, `tools/process_registry.py`, `pyproject.toml`, `README.md`, and 4 docs pages. Credits to everyone whose prior PR work informed these fixes — see the co-author trailers. All of the PRs listed in `~/.hermes/plans/windows-support-prs.md` fixing `os.kill` / `signal.SIGKILL` / UTF-8 stdio / tzdata / README patterns found the same issues; this PR consolidates them. Co-authored-by: Philip D'Souza <9472774+PhilipAD@users.noreply.github.com> Co-authored-by: Arecanon <42595053+ArecaNon@users.noreply.github.com> Co-authored-by: XiaoXiao0221 <263113677+XiaoXiao0221@users.noreply.github.com> Co-authored-by: Lars Hagen <1360677+lars-hagen@users.noreply.github.com> Co-authored-by: Luan Dias <65574834+luandiasrj@users.noreply.github.com> Co-authored-by: Ruzzgar <ruzzgarcn@gmail.com> Co-authored-by: sprmn24 <oncuevtv@gmail.com> Co-authored-by: adybag14-cyber <252811164+adybag14-cyber@users.noreply.github.com> Co-authored-by: Prasanna28Devadiga <54196612+Prasanna28Devadiga@users.noreply.github.com>
8.6 KiB
| sidebar_position | title | description |
|---|---|---|
| 4 | Contributing | How to contribute to Hermes Agent — dev setup, code style, PR process |
Contributing
Thank you for contributing to Hermes Agent! This guide covers setting up your dev environment, understanding the codebase, and getting your PR merged.
Contribution Priorities
We value contributions in this order:
- Bug fixes — crashes, incorrect behavior, data loss
- Cross-platform compatibility — macOS, different Linux distros, WSL2
- Security hardening — shell injection, prompt injection, path traversal
- Performance and robustness — retry logic, error handling, graceful degradation
- New skills — broadly useful ones (see Creating Skills)
- New tools — rarely needed; most capabilities should be skills
- Documentation — fixes, clarifications, new examples
Common contribution paths
- Building a custom/local tool without modifying Hermes core? Start with Build a Hermes Plugin
- Building a new built-in core tool for Hermes itself? Start with Adding Tools
- Building a new skill? Start with Creating Skills
- Building a new inference provider? Start with Adding Providers
Development Setup
Prerequisites
| Requirement | Notes |
|---|---|
| Git | With --recurse-submodules support, and the git-lfs extension installed |
| Python 3.11+ | uv will install it if missing |
| uv | Fast Python package manager (install) |
| Node.js 20+ | Optional — needed for browser tools and WhatsApp bridge (matches root package.json engines) |
Clone and Install
git clone --recurse-submodules https://github.com/NousResearch/hermes-agent.git
cd hermes-agent
# Create venv with Python 3.11
uv venv venv --python 3.11
export VIRTUAL_ENV="$(pwd)/venv"
# Install with all extras (messaging, cron, CLI menus, dev tools)
uv pip install -e ".[all,dev]"
uv pip install -e "./tinker-atropos"
# Optional: browser tools
npm install
Configure for Development
mkdir -p ~/.hermes/{cron,sessions,logs,memories,skills}
cp cli-config.yaml.example ~/.hermes/config.yaml
touch ~/.hermes/.env
# Add at minimum an LLM provider key:
echo 'OPENROUTER_API_KEY=sk-or-v1-your-key' >> ~/.hermes/.env
Run
# Symlink for global access
mkdir -p ~/.local/bin
ln -sf "$(pwd)/venv/bin/hermes" ~/.local/bin/hermes
# Verify
hermes doctor
hermes chat -q "Hello"
Run Tests
pytest tests/ -v
Code Style
- PEP 8 with practical exceptions (no strict line length enforcement)
- Comments: Only when explaining non-obvious intent, trade-offs, or API quirks
- Error handling: Catch specific exceptions. Use
logger.warning()/logger.error()withexc_info=Truefor unexpected errors - Cross-platform: Never assume Unix (see below)
- Profile-safe paths: Never hardcode
~/.hermes— useget_hermes_home()fromhermes_constantsfor code paths anddisplay_hermes_home()for user-facing messages. See AGENTS.md for full rules.
Cross-Platform Compatibility
Hermes officially supports Linux, macOS, WSL2, and native Windows (via PowerShell install). Native Windows uses Git Bash (from Git for Windows) for shell commands. A few features require POSIX kernel primitives and are gated: the dashboard's embedded PTY terminal pane (/chat tab) is WSL2-only.
When contributing code, keep these rules in mind:
- Don't add unguarded
signal.SIGKILLreferences. It's not defined on Windows. Either route throughgateway.status.terminate_pid(pid, force=True)(the centralized primitive that doestaskkill /T /Fon Windows and SIGKILL on POSIX), or fall back withgetattr(signal, "SIGKILL", signal.SIGTERM). - Catch
OSErroralongsideProcessLookupErroronos.kill(pid, 0)probes. Windows raisesOSError(WinError 87, "parameter is incorrect") for an already-gone PID instead ofProcessLookupError. - Don't force the terminal to POSIX semantics.
os.setsid,os.killpg,os.getpgid,os.forkall raise on Windows — gate them withif sys.platform != "win32":orif os.name != "nt":. - Open files with an explicit
encoding="utf-8". The Python default on Windows is the system locale (often cp1252), which mojibakes or crashes on non-Latin text. - Use
pathlib.Path/os.path.join— never manually concat with/. This matters less for strings the OS gives us back and more for strings we construct to hand to subprocesses.
Key patterns:
1. termios and fcntl are Unix-only
Always catch both ImportError and NotImplementedError:
try:
from simple_term_menu import TerminalMenu
menu = TerminalMenu(options)
idx = menu.show()
except (ImportError, NotImplementedError):
# Fallback: numbered menu
for i, opt in enumerate(options):
print(f" {i+1}. {opt}")
idx = int(input("Choice: ")) - 1
2. File encoding
Some environments may save .env files in non-UTF-8 encodings:
try:
load_dotenv(env_path)
except UnicodeDecodeError:
load_dotenv(env_path, encoding="latin-1")
3. Process management
os.setsid(), os.killpg(), and signal handling differ across platforms:
import platform
if platform.system() != "Windows":
kwargs["preexec_fn"] = os.setsid
4. Path separators
Use pathlib.Path instead of string concatenation with /.
Security Considerations
Hermes has terminal access. Security matters.
Existing Protections
| Layer | Implementation |
|---|---|
| Sudo password piping | Uses shlex.quote() to prevent shell injection |
| Dangerous command detection | Regex patterns in tools/approval.py with user approval flow |
| Cron prompt injection | Scanner blocks instruction-override patterns |
| Write deny list | Protected paths resolved via os.path.realpath() to prevent symlink bypass |
| Skills guard | Security scanner for hub-installed skills |
| Code execution sandbox | Child process runs with API keys stripped |
| Container hardening | Docker: all capabilities dropped, no privilege escalation, PID limits |
Contributing Security-Sensitive Code
- Always use
shlex.quote()when interpolating user input into shell commands - Resolve symlinks with
os.path.realpath()before access control checks - Don't log secrets
- Catch broad exceptions around tool execution
- Test on all platforms if your change touches file paths or processes
Pull Request Process
Branch Naming
fix/description # Bug fixes
feat/description # New features
docs/description # Documentation
test/description # Tests
refactor/description # Code restructuring
Before Submitting
- Run tests:
pytest tests/ -v - Test manually: Run
hermesand exercise the code path you changed - Check cross-platform impact: Consider macOS and different Linux distros
- Keep PRs focused: One logical change per PR
PR Description
Include:
- What changed and why
- How to test it
- What platforms you tested on
- Reference any related issues
Commit Messages
We use Conventional Commits:
<type>(<scope>): <description>
| Type | Use for |
|---|---|
fix |
Bug fixes |
feat |
New features |
docs |
Documentation |
test |
Tests |
refactor |
Code restructuring |
chore |
Build, CI, dependency updates |
Scopes: cli, gateway, tools, skills, agent, install, whatsapp, security
Examples:
fix(cli): prevent crash in save_config_value when model is a string
feat(gateway): add WhatsApp multi-user session isolation
fix(security): prevent shell injection in sudo password piping
Reporting Issues
- Use GitHub Issues
- Include: OS, Python version, Hermes version (
hermes version), full error traceback - Include steps to reproduce
- Check existing issues before creating duplicates
- For security vulnerabilities, please report privately
Community
- Discord: discord.gg/NousResearch
- GitHub Discussions: For design proposals and architecture discussions
- Skills Hub: Upload specialized skills and share with the community
License
By contributing, you agree that your contributions will be licensed under the MIT License.