sidebar_position: 4 title: "Contributing" description: "How to contribute to Hermes Agent — dev setup, code style, PR process"
Thank you for contributing to Hermes Agent! This guide covers setting up your dev environment, understanding the codebase, and getting your PR merged.
We value contributions in this order:
| Requirement | Notes |
|---|---|
| Git | With the git-lfs extension installed |
| Python 3.11–3.13 | uv will install it if missing |
| uv | Fast Python package manager (install) |
| Node.js 26+ | Optional — needed for browser tools and WhatsApp bridge (matches root package.json engines) |
For most contributors, the best development bootstrap is the same path users
take: run the standard installer, then work inside the repository it cloned.
The installer creates the Hermes venv, wires the hermes command, stamps the
install method for hermes update, and clones the full git project into
$HERMES_HOME/hermes-agent (usually ~/.hermes/hermes-agent). That keeps your
development environment on the same layout the CLI, updater, lazy dependency
installer, gateway, and docs assume.
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
cd "${HERMES_HOME:-$HOME/.hermes}/hermes-agent"
# Add dev/test extras on top of the standard install.
uv pip install -e ".[all,dev]"
# Optional: browser tools / docs site dependencies.
npm install
After that, create branches and run tests from that checkout:
git checkout -b fix/description
scripts/run_tests.sh
You can also run a fully isolated Hermes instance (throwaway HERMES_HOME, separate Electron userData, distinct Electron app name to avoid the single-instance lock):
scripts/dev-sandbox.sh python -m hermes_cli.main
scripts/dev-sandbox.sh --persistent python -m hermes_cli.main desktop # state survives restarts, but lives in the worktree :)
Use this only if you intentionally do not want Hermes' managed install layout
(for example, a throwaway clone inside a container or CI job). If you install
this way, make sure you run the hermes entrypoint from this venv; running the
system python3 -m hermes_cli.main can pick up unrelated system Python
packages.
Create the venv outside the cloned source tree. A venv that lives inside
the directory the agent operates from can be wiped by a relative-path command
the agent runs against its own checkout (rm -rf venv, uv venv venv, etc.),
which silently destroys the running runtime mid-session. Keeping it outside the
tree means no relative path from the workspace resolves to it.
git clone https://github.com/NousResearch/hermes-agent.git
cd hermes-agent
# Create venv with Python 3.11, OUTSIDE the source tree
uv venv ~/.hermes/venvs/hermes-dev --python 3.11
export VIRTUAL_ENV="$HOME/.hermes/venvs/hermes-dev"
export PATH="$VIRTUAL_ENV/bin:$PATH"
# Install with all extras (messaging, cron, CLI menus, dev tools)
uv pip install -e ".[all,dev]"
# Optional: browser tools
npm install
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
# The standard installer already put `hermes` on PATH.
hermes doctor
hermes chat -q "Hello"
If you used the manual clone fallback, run ./hermes from the checkout or
symlink this clone's venv explicitly:
mkdir -p ~/.local/bin
ln -sf "$(pwd)/venv/bin/hermes" ~/.local/bin/hermes
scripts/run_tests.sh
logger.warning()/logger.error() with exc_info=True for unexpected errors~/.hermes — use get_hermes_home() from hermes_constants for code paths and display_hermes_home() for user-facing messages. See AGENTS.md for full rules.See Platform Support. 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) needs a POSIX PTY (Linux, macOS, or WSL2). 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:
signal.SIGKILL references. It's not defined on Windows. Either route through gateway.status.terminate_pid(pid, force=True) (the centralized primitive that does taskkill /T /F on Windows and SIGKILL on POSIX), or fall back with getattr(signal, "SIGKILL", signal.SIGTERM).OSError alongside ProcessLookupError on os.kill(pid, 0) probes. Windows raises OSError (WinError 87, "parameter is incorrect") for an already-gone PID instead of ProcessLookupError.os.setsid, os.killpg, os.getpgid, os.fork all raise on Windows — gate them with if sys.platform != "win32": or if os.name != "nt":.encoding="utf-8". The Python default on Windows is the system locale (often cp1252), which mojibakes or crashes on non-Latin text.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:
termios and fcntl are Unix-onlyAlways 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
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")
os.setsid(), os.killpg(), and signal handling differ across platforms:
import platform
if platform.system() != "Windows":
kwargs["preexec_fn"] = os.setsid
Use pathlib.Path instead of string concatenation with /.
Hermes has terminal access. Security matters.
| 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 |
shlex.quote() when interpolating user input into shell commandsos.path.realpath() before access control checksfix/description # Bug fixes
feat/description # New features
docs/description # Documentation
test/description # Tests
refactor/description # Code restructuring
scripts/run_tests.sh for CI-parity. Use direct python -m pytest ... only when the wrapper is unavailable or you are intentionally debugging outside the wrapper.hermes and exercise the code path you changedscripts/check-windows-footguns.py.Include:
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
hermes version), full error tracebackBy contributing, you agree that your contributions will be licensed under the MIT License.