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:

  1. Install py311-sqlite3 before running Hermes.
  2. Do not expect obsidian-headless to install on FreeBSD.
  3. Use Syncthing/Möbius Sync for Obsidian instead.
  4. Do not use daemon -r for Hermes inside a jail service.
  5. Keep Hermes writing to a dedicated Obsidian folder first.
  6. 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.