Finish bootstrap uv before PM replaces its store entry. Keep failure receipts stdlib-only and align the cryptography requirement and override with the locked version. Let bundle builders declare launch paths and update ownership. Remove payload discovery, Store probing, and the unused develop command. Derive Nix Python from the PM lock and share its provenance stamp. Document setup, activation, optional dependencies, and distribution ownership. Targeted Windows tests, relocated runtime launches, Electron bundling, and bilingual docs builds pass. Native Nix and signed-package acceptance remain CI gates.
14 KiB
sidebar_position, title, description
| 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 the git-lfs extension installed |
| Python 3.14 | The project requires >=3.14,<3.15. PM provides the pinned interpreter. |
| uv | Fast Python package manager (install) |
| Node.js | Use the PM pin or a version accepted by root package.json engines |
PM developer environment
Use the PM developer workflow for preparation, activation, everyday commands, dependency changes, and test environments. Select your development home before setup so experimental code does not migrate production data.
After successful setup, activate from the repository root in each new shell.
Bash:
source ./activate
python hermes --version
PowerShell:
. .\activate.ps1
python hermes --version
Run python hermes for this checkout, not a global hermes alias. PM activation
adds installed tools and the selected dependency tree. It does not install
packages or JS workspaces. deactivate restores the prior shell environment.
Manual development and test environment
Use Python 3.14 (>=3.14,<3.15). Keep a development environment outside the
source tree if an agent will operate on that checkout. Leave PM activation
before this sequence. Keep the same development HERMES_HOME when running it.
This environment is for tests and editor tools. Its own interpreter includes
pytest without relying on PM's PYTHONPATH. On Windows, initialize the native
C++ build environment for your architecture before building source dependencies.
POSIX:
uv venv "$HOME/.hermes/venvs/hermes-dev" --python 3.14
export UV_PROJECT_ENVIRONMENT="$HOME/.hermes/venvs/hermes-dev"
uv sync --locked --extra all --extra dev
export HERMES_PYTHON="$UV_PROJECT_ENVIRONMENT/bin/python"
"$HERMES_PYTHON" hermes --version
PowerShell:
$devEnv = Join-Path $env:LOCALAPPDATA 'hermes-dev-env'
uv venv $devEnv --python 3.14
$env:UV_PROJECT_ENVIRONMENT = $devEnv
uv sync --locked --extra all --extra dev
$env:HERMES_PYTHON = Join-Path $devEnv 'Scripts/python.exe'
& $env:HERMES_PYTHON hermes --version
This environment is for source development and tests. It does not replace
PM's tool store or a packaged app's dependency selection.
Run uv pip check --python with this environment's interpreter to check its dependencies.
Do not install into an MSIX payload or point a bundled app at this environment.
For an isolated development instance, select a disposable HERMES_HOME before
starting the source command. Use python hermes setup to configure it rather
than copying production credentials into the checkout.
JavaScript workspaces and website
From the repository root, run npm ci for the desktop, TUI, dashboard, and
shared JS workspaces. The website is separate:
npm ci --prefix website
npm run build:fast --prefix website
Use a Node/npm version accepted by the corresponding package.json engines.
Native desktop dependencies can also require the platform build toolchain.
Logos and icons are generated from assets/nous-girl-*.svg and
assets/backgrounds/. node scripts/generate-icons.mjs uses the locked,
isolated icon-build dependency group. Do not commit generated PNG/ICO/ICNS
outputs or add icon renderers to production dependencies.
Run tests
Use the canonical runner on every host:
scripts/run_tests.sh
scripts/run_tests.sh tests/agent/ -v
On Windows, run the script through Bash. When no local .venv or venv
contains pytest, the runner accepts the explicit HERMES_PYTHON above. It
clears credentials, isolates HERMES_HOME, and runs each test file in a separate
subprocess through scripts/run_tests_parallel.py. It does not use xdist.
Run the relevant JS workspace checks for JS changes. Native install/update E2E runs on disposable CI hosts, never against the developer's live app. See Package management for PM commands and runtime ownership.
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
See Platform Support. Native Windows uses Git Bash (from Git for Windows) for shell commands. The dashboard uses POSIX PTYs on Unix and the pywinpty/ConPTY bridge on Windows. Availability depends on that host's native dependency support. If you're doing Windows-heavy dev, run the Windows-footgun lint (scripts/check-windows-footguns.py) before pushing.
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). - Use
psutil.pid_exists()for process liveness. Do not useos.kill(pid, 0)on Windows; it is not a safe probe. - 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":. - Use explicit text encodings. User-authored UTF-8 reads use
utf-8-sigto accept a leading BOM. Writes useutf-8without adding a BOM. - 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. 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")
2. Process management
os.setsid(), os.killpg(), and signal handling differ across platforms:
import platform
if platform.system() != "Windows":
kwargs["preexec_fn"] = os.setsid
3. 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:
scripts/run_tests.shfor CI-parity. Use directpython -m pytest ...only when the wrapper is unavailable or you are intentionally debugging outside the wrapper. - Test manually: Run
hermesand exercise the code path you changed - Check cross-platform impact: Consider macOS, Linux, WSL2, and native Windows. If you touch file I/O, process management, terminal handling, subprocesses, or signals, run
scripts/check-windows-footguns.py. - 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
Repo-local review checklists: .agents/checks/*.md
Projects built on (or reviewed by) Hermes can keep reviewer checklists inside the repository under .agents/checks/. Each file is a focused, plain-markdown checklist that an agent loads before reviewing a change touching the matching area:
.agents/
checks/
security.md # e.g. "grep the diff for shell interpolation; check subprocess calls quote args"
migrations.md # e.g. "every schema change ships a backfill and a rollback note"
public-api.md # e.g. "exported signatures changed? flag for semver review"
Conventions that make these work well:
- One concern per file, named after the concern. Small files get read in full; a monolithic
checklist.mdgets skimmed. - Write checks as verifiable actions ("run X and confirm Y"), not aspirations ("code should be secure").
- State the trigger at the top — which paths or change types the checklist applies to — so an agent (or human) can skip irrelevant ones cheaply.
- Keep them in version control next to the code they guard: they evolve with the codebase, and a PR that changes the rules changes the checklist in the same diff.
When you ask Hermes to review a PR in a repository that has .agents/checks/, tell it (or teach it via a skill) to read the relevant checklists first and report against them. This gives review agents the project-specific bar that generic review prompts miss.
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.