Hermes Agent on FreeBSD 15 Jail with Telegram and Obsidian Sync
I wanted a small, always-on AI agent running on my FreeBSD server, controlled from Telegram on iOS, with access to an Obsidian vault. The final setup works nicely, but the path there had a few FreeBSD-specific surprises.
Target Setup
The working architecture looks like this:
Telegram on iOS
⇅
Hermes Agent gateway
⇅
FreeBSD 15 jail
⇅
Obsidian Markdown vault
⇅
Syncthing / Möbius Sync / macOS Syncthing
⇅
Obsidian on iOS and macOS
Hermes runs inside a dedicated FreeBSD jail. Telegram is the mobile interface. Obsidian stays a normal Markdown vault, synced with Syncthing-compatible tooling instead of Obsidian Headless.
Base Jail
I created a normal FreeBSD jail for Hermes rather than running it directly on the host:
sysrc jail_enable="YES"
sysrc jail_parallel_start="YES"
mkdir -p /usr/local/jails/containers/hermes
tar -xf /usr/local/jails/media/15.0-RELEASE-base.txz \
-C /usr/local/jails/containers/hermes --unlink
cp /etc/resolv.conf /usr/local/jails/containers/hermes/etc/resolv.conf
cp /etc/localtime /usr/local/jails/containers/hermes/etc/localtime
Example jail config:
hermes {
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
exec.consolelog = "/var/log/jail_console_${name}.log";
exec.clean;
mount.devfs;
allow.raw_sockets;
host.hostname = "hermes";
path = "/usr/local/jails/containers/hermes";
ip4 = inherit;
}
Start it:
service jail start hermes
jexec -u root hermes /bin/sh
Hermes Dependencies
Inside the jail:
pkg bootstrap -y
pkg update
pkg install -y \
bash ca_root_nss curl git gmake pkgconf cmake rust \
python311 py311-pip py311-sqlite3 sqlite3 \
uv node24 npm-node24 ripgrep ffmpeg
The important FreeBSD-specific package here is py311-sqlite3.
Without it, hermes doctor failed with:
ModuleNotFoundError: No module named '_sqlite3'
That cannot be fixed with pip; _sqlite3 is a compiled
Python stdlib extension supplied by the FreeBSD package.
Create the Hermes user:
pw useradd -n hermes -m -s /usr/local/bin/bash
su - hermes
Install Hermes:
git clone https://github.com/NousResearch/hermes-agent.git ~/.hermes/hermes-agent
cd ~/.hermes/hermes-agent
uv venv venv --python /usr/local/bin/python3.11
. venv/bin/activate
uv pip install -e ".[messaging,cli,pty]"
mkdir -p ~/.local/bin
ln -sf ~/.hermes/hermes-agent/venv/bin/hermes ~/.local/bin/hermes
cat >> ~/.profile <<'EOF'
export PATH="$HOME/.local/bin:$HOME/.hermes/hermes-agent/venv/bin:$PATH"
export HERMES_HOME="$HOME/.hermes"
EOF
. ~/.profile
hermes doctor
Telegram Setup
On iOS, I created a bot with @BotFather and copied the
bot token.
Then I got my numeric Telegram user ID with @userinfobot.
Hermes wants the numeric ID, not the @username.
The resulting ~/.hermes/.env contains:
TELEGRAM_BOT_TOKEN=your-new-token-here
TELEGRAM_ALLOWED_USERS=123456789
TELEGRAM_HOME_CHANNEL=123456789
If a token ever appears in terminal output or screenshots, revoke it
immediately with BotFather’s /revoke command.
Run the gateway manually first:
su - hermes
cd ~/.hermes/hermes-agent
. venv/bin/activate
hermes gateway run
Then send the bot a test message from Telegram.
FreeBSD rc.d Service for Hermes Gateway
The tricky part was getting the gateway to behave well as a jail service.
Using daemon -r caused shutdown problems because it
respawned Hermes while the jail was trying to stop. Using
daemon -u hermes also produced:
daemon: failed to set user environment
The stable solution was to use a small launcher script and a non-respawning rc.d service.
Create /usr/local/sbin/hermes-gateway-run:
cat > /usr/local/sbin/hermes-gateway-run <<'EOF'
#!/bin/sh
export HOME="/home/hermes"
export HERMES_HOME="/home/hermes/.hermes"
export PATH="/home/hermes/.local/bin:/home/hermes/.hermes/hermes-agent/venv/bin:/usr/local/bin:/usr/bin:/bin"
cd /home/hermes/.hermes/hermes-agent || exit 1
exec /home/hermes/.local/bin/hermes gateway run
EOF
chmod +x /usr/local/sbin/hermes-gateway-run
Create /usr/local/etc/rc.d/hermes_gateway:
cat > /usr/local/etc/rc.d/hermes_gateway <<'EOF'
#!/bin/sh
# PROVIDE: hermes_gateway
# REQUIRE: LOGIN NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="hermes_gateway"
rcvar="hermes_gateway_enable"
load_rc_config $name
: ${hermes_gateway_enable:="NO"}
: ${hermes_gateway_user:="hermes"}
: ${hermes_gateway_home:="/home/hermes"}
pidfile="${hermes_gateway_home}/.hermes/run/${name}.pid"
logfile="${hermes_gateway_home}/.hermes/logs/rc-gateway.log"
start_precmd="${name}_prestart"
stop_cmd="${name}_stop"
status_cmd="${name}_status"
hermes_gateway_prestart()
{
install -d -o ${hermes_gateway_user} -g ${hermes_gateway_user} ${hermes_gateway_home}/.hermes/run
install -d -o ${hermes_gateway_user} -g ${hermes_gateway_user} ${hermes_gateway_home}/.hermes/logs
rm -f "${pidfile}"
}
hermes_gateway_stop()
{
echo "Stopping ${name}."
pkill -TERM -f "hermes gateway run" 2>/dev/null || true
pkill -TERM -f "hermes-gateway-run" 2>/dev/null || true
pkill -TERM -f "su -m ${hermes_gateway_user}" 2>/dev/null || true
sleep 3
pkill -KILL -f "hermes gateway run" 2>/dev/null || true
pkill -KILL -f "hermes-gateway-run" 2>/dev/null || true
pkill -KILL -f "su -m ${hermes_gateway_user}" 2>/dev/null || true
rm -f "${pidfile}"
}
hermes_gateway_status()
{
pgrep -af "hermes gateway run" >/dev/null 2>&1 && {
echo "${name} is running."
return 0
}
echo "${name} is not running."
return 1
}
command="/usr/sbin/daemon"
command_args="-f -p ${pidfile} -o ${logfile} /usr/bin/su -m ${hermes_gateway_user} -c /usr/local/sbin/hermes-gateway-run"
run_rc_command "$1"
EOF
chmod +x /usr/local/etc/rc.d/hermes_gateway
sysrc hermes_gateway_enable="YES"
service hermes_gateway start
Check logs:
tail -f /home/hermes/.hermes/logs/rc-gateway.log
Stop test:
service hermes_gateway stop
service hermes_gateway status
Only after that worked cleanly did I test jail shutdown from the host:
service jail stop hermes
service jail start hermes
Obsidian Integration
Initially I tried Obsidian Headless:
npm install -g obsidian-headless
On FreeBSD this failed with:
npm error code EBADPLATFORM
npm error notsup Unsupported platform for [email protected]
npm error notsup Valid os: darwin,linux,win32
npm error notsup Actual os: freebsd
So the better FreeBSD-native solution was not Obsidian Headless.
The working approach:
Obsidian iOS
normal vault created first
then exposed via external app folder
⇅
Möbius Sync on iOS
⇅
Syncthing
⇅
macOS Syncthing
⇅
FreeBSD Hermes jail vault folder
In the jail, I use:
su - hermes
mkdir -p ~/vaults/personal/Hermes/Inbox
mkdir -p ~/vaults/personal/Hermes/Daily
mkdir -p ~/vaults/personal/Hermes/Research
mkdir -p ~/vaults/personal/Hermes/Tasks
mkdir -p ~/vaults/personal/Hermes/Attachments
Then I created a small rules file for Hermes:
cat > ~/vaults/personal/Hermes/README-HERMES.md <<'EOF'
# Hermes Obsidian Rules
Hermes may read this vault for context.
Hermes may write freely only inside:
- Hermes/Inbox
- Hermes/Daily
- Hermes/Research
- Hermes/Tasks
- Hermes/Attachments
Hermes must not delete notes.
Hermes must not rename notes unless explicitly asked.
Hermes must ask before editing files outside Hermes/.
New notes should use Markdown.
Prefer wiki links when useful.
EOF
From Telegram, I told Hermes:
My Obsidian vault is at /home/hermes/vaults/personal.
Read /home/hermes/vaults/personal/Hermes/README-HERMES.md and follow it.
For Obsidian work:
- write new notes only under Hermes/
- do not delete notes
- do not rename notes unless I explicitly ask
- create drafts in Hermes/Inbox when unsure
Test prompt:
Create a note at /home/hermes/vaults/personal/Hermes/Inbox/test-from-telegram.md saying the Obsidian sync integration works.
Syncthing Ignore Patterns
For the Obsidian vault, I ignore volatile workspace files:
.obsidian/workspace*
.obsidian/cache
.trash/
.DS_Store
I do not ignore all of .obsidian/, because syncing
plugins, themes, and settings may be useful.
Lessons Learned
The main FreeBSD-specific notes:
- Install
py311-sqlite3before running Hermes. - Do not expect
obsidian-headlessto install on FreeBSD. - Use Syncthing/Möbius Sync for Obsidian instead.
- Do not use
daemon -rfor Hermes inside a jail service. - Keep Hermes writing to a dedicated Obsidian folder first.
- Revoke Telegram bot tokens if they appear in screenshots or logs.
The final setup is simple and reliable: Hermes lives in a FreeBSD jail, Telegram gives me mobile control, and Obsidian remains just a synced Markdown vault.