Hermes Agent on Mac: a local-first setup with two profiles, A (local) and B (cloud)
Who is this for? macOS Apple Silicon users who want a sovereign, local-first AI assistant for their sensitive data, AND a high-performance cloud mode for the tasks that earn it. Technical readers comfortable in the terminal, who are not afraid to edit a
config.yaml.
TL;DR
| Mode A (local) | Mode B (cloud) | |
|---|---|---|
| Model | gpt-oss:20b (Ollama, daily driver); override to gemma4:26b | MiniMax-M3 (minimax-oauth) plus a fallback chain |
| Network | None. Strictly local. | Cloud (Claude, Gemini, Nous, OpenRouter) |
| Sensitive data | Yes | Never |
| Toolset | Light (6 tools, ~83 KB prompt) | Full |
| Performance | ~40 s first reply (gpt-oss), ~80 s (gemma4:26b) | <5 s (cloud) |
| Switch | ops/hermes-mode.sh a | ops/hermes-mode.sh b |
Three principles of the setup:
- Code lives in
~/DEV/hermes-agent(Git-versioned), secrets in~/.hermes(out of the repo) - Two native Hermes profiles:
default(local) andcloud, each with its own isolatedHERMES_HOME - A dedicated work directory (
~/DEV/hermes-work/) with its ownAGENTS.md, not run from the repo
Part 1: Installation
Why two profiles instead of one?
Before starting, I hesitated. A single profile with hermes chat -m <model> to
switch model on the fly is tempting. Digging in, I understood that Hermes
profiles are not just about picking a model. Each profile is a completely
isolated HERMES_HOME:
config.yaml,.env, memory, sessions, sandbox, skills: everything is duplicated- the cwd’s
AGENTS.mdis read separately - tools can diverge between profiles
What that buys you, concretely:
- Secret isolation: if a prompt injection reads the
.envin Mode A, it finds no Claude/Gemini/OpenRouter token - Safe default posture: Mode A is active at startup, so I have to switch out explicitly
- Sandbox isolation: my cloud work does not pollute my local files
- Divergent toolsets: Mode A can be slimmed for speed without touching Mode B
Nous Research documents this on purpose: “profiles are independent islands on
purpose” (see the repo’s AGENTS.md, line 138).
Why separate code and secrets?
~/DEV/hermes-agent is a Git repo (Nous Research’s own, forkable). If I put my
API keys in it, git status shows them and a careless git add commits them.
The fix: everything secret or shared lives in ~/.hermes/, never in the repo.
That is the convention the official bootstrap follows.
Prerequisites
- macOS Apple Silicon (tested on an M2 Max)
- Docker Desktop running (active icon in the menu bar): the sandbox execution backend
- Ollama installed (
brew install ollama) - a
zshterminal
docker info # must print server info, not a connection error
ollama --version
Build and install
# Build tools
curl -LsSf https://astral.sh/uv/install.sh | sh
brew install node
exec $SHELL -l
# Clone (submodules = third-party plugins/skills)
mkdir -p ~/DEV
git clone --recurse-submodules https://github.com/NousResearch/hermes-agent.git ~/DEV/hermes-agent
cd ~/DEV/hermes-agent
# venv + editable install (Python 3.11 pinned)
uv venv venv --python 3.11
export VIRTUAL_ENV="$(pwd)/venv"
uv pip install -e ".[all,dev]"
# Local browser (used by some Mode A tools)
npm install
npx playwright install chromium
# Expose the venv launcher (NOT the repo wrapper)
mkdir -p ~/.local/bin
ln -sf "$(pwd)/venv/bin/hermes" ~/.local/bin/hermes
grep -q 'HOME/.local/bin' ~/.zshrc || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
exec $SHELL -l
hermes version # verify the install
Initial configuration
hermes setup # interactive wizard
hermes doctor # full health check
Keys and identities: where each thing lives
Mode A, ~/.hermes/.env (the real sovereign posture, kept clean):
TAVILY_API_KEY=tvly-xxxx # web search
FIRECRAWL_API_KEY=fc-xxxx # page extraction
GITHUB_TOKEN=ghp_xxxx # Skills Hub rate limit (optional)
TELEGRAM_BOT_TOKEN=... # fine: chat channel, not model auth
TELEGRAM_ALLOWED_USERS=...
TELEGRAM_HOME_CHANNEL=...
TERMINAL_ENV=docker
# Deliberately absent: OPENROUTER_API_KEY, EXA_API_KEY, GOOGLE_API_KEY,
# ANTHROPIC_TOKEN. Every cloud key lives in profiles/cloud/.env.
Mode B, ~/.hermes/profiles/cloud/.env (and/or via OAuth login):
| Provider | Auth | Command |
|---|---|---|
| MiniMax-M3 (current primary) | OAuth (minimax-oauth) | hermes -p cloud model |
| Claude (Anthropic) | OAuth (Claude Pro) | hermes -p cloud model then Anthropic then browser login |
| Google Gemini | API key (GEMINI_API_KEY) | Deliberate choice: Google discourages Gemini OAuth inside third-party agents |
| Nous Portal | OAuth | hermes login |
| OpenRouter | API key | Wide safety net |
ToS note: using a Claude Pro subscription via OAuth inside a third-party agent is a grey area. It works, but the official path is the Anthropic API key (same as for Google Gemini).
Execution backend: Docker
hermes setup tools # Execution / Terminal backend then docker
hermes config set terminal.backend docker # verify
The default image is nikolaik/python-nodejs:python3.11-nodejs20. Sandboxes
persist their files in ~/.hermes/sandboxes/<profile>/.
Models: choose one per use
Mode A: two models for two uses
| Model | Size | First-reply time | Use |
|---|---|---|---|
gpt-oss:20b | 13 GB | ~40 s | Daily driver: Excel, CSV, transforms, debug |
gemma4:26b-a4b-it-q4_K_M | 17 GB | ~80 s | Sensitive research, long reasoning |
Why two models? At first I ran only the 26B Gemma. The problem: on an M2 Max, a forward pass through 26B parameters saturates compute before prompt size matters. Measured: full toolset vs light toolset = 1m vs 1m20s, inside the margin of error. The real local-speed lever is model size.
Setup:
# Check what is installed
ollama list
# Pull what is missing (gpt-oss:20b is 13 GB, gemma4:26b is 17 GB)
ollama pull gpt-oss:20b
ollama pull gemma4:26b-a4b-it-q4_K_M
# Set gpt-oss:20b as Mode A primary
hermes -p default model # interactive picker
# One-off override to the 26B (sensitive research):
hermes -p default chat -m gemma4:26b-a4b-it-q4_K_M
Context-window trap: Hermes Agent requires at least 64K tokens of context. Several 7B models (like
qwen2.5-coder:7b) cap at 32K and are rejected at init. Always check before adopting a model:ollama show <model>orollama run <model> /show info.
Mode B: primary plus fallback chain
Current primary: MiniMax-M3 (provider minimax-oauth, base
https://api.minimax.io/anthropic). Ordered fallback chain:
Primary: MiniMax-M3 (via minimax-oauth)
1. claude-opus-4-8 (via anthropic, Claude Pro OAuth or API key)
2. gemini-3.5-flash (via gemini, Google API key)
3. stepfun/step-3.7-flash:free (via nous)
4. openai/gpt-5.5 (via openrouter, API key)
The order is managed with:
hermes -p cloud fallback list # inspect
hermes -p cloud fallback add # add an entry (validated picker)
hermes -p cloud fallback remove # remove an entry
hermes -p cloud fallback clear # start fresh
People ask why MiniMax-M3 over Claude Opus 4.8. Two reasons. Cost first, and it is not close. Then concentration risk: leaning the whole stack on a single vendor, in a single jurisdiction, is exactly the dependency I spend my fractional-CIO days helping clients design out. A second provider in a different orbit is cheap insurance, not a downgrade.
Setting up the two profiles
The ops/hermes-mode.sh wrapper ships with the repo and simplifies the switch.
For Mode B:
# One-time: create the isolated 'cloud' profile
ops/hermes-mode.sh bootstrap
# Cloud logins (one-time, interactive)
hermes -p cloud model # MiniMax-M3 primary, then Claude OAuth if needed
# Put API keys in ~/.hermes/profiles/cloud/.env:
# GEMINI_API_KEY=...
# OPENROUTER_API_KEY=...
# Fallback chain
hermes -p cloud fallback clear
hermes -p cloud fallback add # Gemini, then Nous, then OpenRouter
hermes -p cloud fallback list
Part 2: Daily use
The trap: NEVER run hermes chat from the repo
If you cd ~/DEV/hermes-agent && hermes chat, the repo’s AGENTS.md (69 KB, a
dev guide for Hermes contributors) is auto-injected, then truncated to
~31 KB. The result: a constant warning,
Context file AGENTS.md TRUNCATED: 69356 chars exceeds limit of 31457, and you
burn prompt, so tokens, for nothing.
Fix: create a dedicated work directory.
mkdir -p ~/DEV/hermes-work
Put an AGENTS.md in it that sounds like you: general-purpose, 1 to 2 KB, with
your working style and constraints. Example:
# User context
You are my personal AI assistant. You help with my daily tasks (data analysis,
research, scripting, debugging, writing, and so on).
## Default posture
- Local mode (default): Ollama via Hermes, no network calls.
- Cloud mode: only on explicit request.
## Working style
- Get to the point. No preamble, no restating the question.
- Answer in French unless the task is in English.
- For code: concrete, runnable snippets.
- If you do not know, say so.
## Security and data
- No destructive command without confirmation.
- No upload off the Mac without explicit request.
- For sensitive files: local processing (pandas/openpyxl).
## Memory and context
- For recurring work, suggest a project name for `hermes sessions --continue`.
Personalize it over time (preferences, business constraints). It is your per-directory context briefing.
A light toolset for Mode A
By default the default profile loads the full toolset (hermes-cli, ~100
tools, ~250 KB of schemas). For Mode A, slim it down.
In ~/.hermes/config.yaml:
platform_toolsets:
cli: [file, memory, session_search, terminal, todo, web]
telegram: [file, memory, session_search, terminal, todo, web]
Measured effect: light toolset = 31 tools, ~83 KB of prompt (vs ~250 KB). The speed gain is marginal on the 26B (compute-bound), but noticeable on gpt-oss:20b. The real benefit is elsewhere: smaller attack surface, more readable logs.
To re-enable everything for one session: cli: [hermes-cli], or switch to
Mode B.
Daily workflow
# A local work session
cd ~/DEV/hermes-work
ops/hermes-mode.sh a # Mode A (local/private)
hermes chat # open the session
# A cloud work session
cd ~/DEV/hermes-work
ops/hermes-mode.sh b # Mode B (cloud)
hermes chat
# One-off overrides (without changing the active profile)
hermes chat -m gemma4:26b-a4b-it-q4_K_M # another model, same profile
hermes chat -p cloud # one Cloud session without switching
hermes chat -p cloud -m claude-opus-4-8 # Cloud + specific model
hermes chat -t terminal,file,memory # ad hoc toolset
The active profile is persistent
Once you run ops/hermes-mode.sh a (or b), every following hermes
inherits the choice, including after you close the terminal. The single source
of truth is ~/.hermes/active_profile. To check:
ops/hermes-mode.sh status
hermes profile
Telegram bot
The Telegram bot is a separate daemon, installed as a launchd service.
Initial setup:
# Mode A by default
ops/hermes-mode.sh a
hermes gateway install # daemon + auto-start at login
hermes gateway start # start now
hermes gateway status # check: "running"
After every A/B switch, restart the gateway so it reloads the config (otherwise it keeps serving with the old profile):
ops/hermes-mode.sh b
hermes gateway restart
On mobile: nothing to configure. Send your bot a message, it answers with the active profile’s model.
Useful commands:
| Action | Command |
|---|---|
| Gateway state | hermes gateway status |
| Stop the bot | hermes gateway stop |
| Restart after a switch | hermes gateway restart |
| Logs | hermes logs gateway |
Cheat sheet
| You want to… | You run… |
|---|---|
| Work locally | cd ~/DEV/hermes-work && ops/hermes-mode.sh a && hermes chat |
| Work in the cloud | cd ~/DEV/hermes-work && ops/hermes-mode.sh b && hermes chat |
| One cloud session | hermes -p cloud chat (active profile unchanged) |
| Change model | hermes chat -m <model-id> |
| Check state | ops/hermes-mode.sh status |
| Switch + Telegram bot | ops/hermes-mode.sh b && hermes gateway restart |
| Stop the bot | hermes gateway stop |
Updating
One command:
hermes update
It updates the repo, the Python deps, and the submodules. No manual git pull
needed.
Where is my data stored?
| What | Where |
|---|---|
| Hermes code | ~/DEV/hermes-agent |
| Mode A config + secrets | ~/.hermes/ |
| Mode B config + secrets | ~/.hermes/profiles/cloud/ |
| Mode A sandbox (Docker bind) | ~/.hermes/sandboxes/ |
| Mode B sandbox (Docker bind) | ~/.hermes/profiles/cloud/sandboxes/ |
| Sessions, memory, skills | Inside each profile (~/.hermes/<profile>/...) |
The Docker image is nikolaik/python-nodejs:python3.11-nodejs20. The
container’s /root home is bind-mapped to the sandbox dir above, so it
persists between sessions.
Periodic validation
ops/hermes-mode.sh status
hermes doctor # Mode A
hermes -p cloud doctor # Mode B
hermes -p cloud fallback list # Mode B fallback chain
hermes prompt-size # Mode A: 31 tools, ~83 KB total prompt
Part 3: Going further
Documented use cases
- Daily Briefing Bot: summarize your morning research https://hermes-agent.nousresearch.com/docs/guides/daily-briefing-bot
- Team Telegram Assistant: a team assistant over Telegram https://hermes-agent.nousresearch.com/docs/guides/team-telegram-assistant
- Python Library: use Hermes as a Python library in your own code https://hermes-agent.nousresearch.com/docs/guides/python-library
- MCP with Hermes: if you really need it https://hermes-agent.nousresearch.com/docs/guides/use-mcp-with-hermes
- More use cases: https://hermes-agent.nousresearch.com/docs/user-stories
Official tips
https://hermes-agent.nousresearch.com/docs/guides/tips
When the CLI wears you down: Hermes Desktop
Once all of this runs and is fully operational, if the CLI starts to wear you
down and you miss a little GUI à la Claude Desktop or Minimax Code, you can
launch hermes desktop and there you go. Same config, same profiles, same
isolated secrets: just a graphical layer on top.
https://hermes-agent.nousresearch.com/docs/user-guide/desktop
Lessons learned
- Hermes profiles are a security boundary, not a model picker. Isolation of secrets, sandboxes, sessions, and a safe default posture.
- The local performance bottleneck is compute (model size), not the prompt. Cutting the toolset from 100 tools to 6 barely moves the clock. The speed lever is the model.
- Always check the context window before adopting a model. Hermes requires at least 64K. 7B models often cap at 32K.
- NEVER run
hermes chatfrom the framework repo. The repo’sAGENTS.md(69 KB) gets injected and truncated. Create a work directory with your own AGENTS.md. - A clean Mode A
.envis the real sovereign posture. Any cloud key in~/.hermes/.envis a leak channel in case of prompt injection.
Conclusion
This setup has run in personal production for a few weeks. Mode A for all
sensitive data (accounting spreadsheets, client CSVs, configs, GDPR inside),
Mode B for public research and the tasks that need a big model. The
ops/hermes-mode.sh wrapper makes the switch transparent. The Telegram bot
gives me the same agent from my phone.
The complexity cost is the initial setup. Day to day, it is just
cd ~/DEV/hermes-work && ops/hermes-mode.sh a && hermes chat.
Note: this is a personal note restructured for future publication. Feedback welcome.