Secure Your Hermes Agent with Signal

Lock down an AI agent with 7 security layers and a Signal gateway.

Introduction

30 Second Summary

An AI agent with terminal access is like leaving your front door unlocked with a robot butler inside. It can do incredible things for you, but without proper guardrails, it can also leak your secrets, obey strangers, or accidentally trash your system.

In this project, you will harden an existing Hermes Agent deployment with multiple layers of security and connect it to Signal so you can securely interact with your agent from your phone over end-to-end encrypted messaging.

What You'll Build

A production-hardened Hermes Agent deployment accessible via Signal's encrypted messaging channel, protected by container isolation, command approval, credential redaction, and strict user authorization.

By the end of this project, you'll have:

  • Docker container isolation as the primary security boundary preventing agent actions from affecting your host system.
  • A command approval system that blocks dangerous operations (like sudo or rm -rf) until you explicitly consent from your phone.
  • A working Signal messaging gateway with strict allowlist authorization so only your phone number can interact with the agent.
  • Secret Mission: Configure a UFW firewall rule and systemd services so your agent survives reboots and rejects all inbound network traffic.

Are there any prerequisites?

Yes. This project assumes you already have a Hermes Agent installed and running in Docker on Ubuntu, Claude Code installed and authenticated with Sonnet, and Signal installed on your mobile phone.

If you haven't set up your Hermes Agent yet, complete the Hermes Agent installation project first.

Install signal-cli and Java 25

Your Hermes Agent is running in Docker on Ubuntu, but right now it has no way to receive messages from the outside world.

In this step, you will install signal-cli, the command-line bridge that connects your agent to Signal's end-to-end encrypted messaging network.

In this step, get ready to:
  • Install Java 25 (Adoptium Temurin) on Ubuntu.
  • Download and install signal-cli v0.14.3 (Linux native build).
  • Link signal-cli to your Signal account as a secondary device.
Install Java 25

signal-cli v0.14.3 requires Java 25. Ubuntu's default repositories do not include Java 25 yet, so you will add the Adoptium Temurin repository first.

  • Install the prerequisite packages for adding external repositories by running this command:
sudo apt install -y wget apt-transport-https gpg
  • Download and trust the Eclipse Adoptium GPG key by running:
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/adoptium.gpg > /dev/null
  • Add the Adoptium apt repository by running:
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list

What does this command do?

This tells your system where to find Adoptium packages. The awk command automatically detects your Ubuntu version codename (like noble or jammy) so the repository URL matches your system.

  • Install Java 25 (Temurin JDK) by running:
sudo apt update && sudo apt install -y temurin-25-jdk
  • Verify the installation by running:
java -version

You should see output starting with openjdk version "25 followed by the Temurin build identifier.

Not seeing Java 25?

Make sure the repository was added correctly. Run cat /etc/apt/sources.list.d/adoptium.list and confirm you see a URL containing your Ubuntu codename.

If apt update showed GPG errors, re-run the GPG key download command above.

help me troubleshoot Java 25 installation on Ubuntu with Adoptium Temurin

Download and install signal-cli v0.14.3

You will use the Linux native build of signal-cli. This is a self-contained GraalVM binary that starts faster and uses less memory than the JVM build.

  • Download the signal-cli native build from GitHub releases by running:
VERSION=0.14.3
curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux-native.tar.gz

What does this command do?

The VERSION variable stores the version number so you can reference it in later commands without retyping it.

The curl -L -O command downloads the native binary archive directly from the signal-cli GitHub releases page. The -L flag follows redirects and -O saves the file with its original name.

  • Extract the archive to /opt by running:
sudo tar xf signal-cli-"${VERSION}"-Linux-native.tar.gz -C /opt
  • Create a symlink so you can run signal-cli from anywhere by running:
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/signal-cli

What does this command do?

The tar command extracts the signal-cli binary into /opt/signal-cli-0.14.3/. The ln -sf command creates a symbolic link at /usr/local/bin/signal-cli pointing to the binary inside the extracted directory, so your shell can find it without the full path.

  • Verify signal-cli is accessible by running:
signal-cli --version

You should see signal-cli 0.14.3 printed in the terminal.

Command not found?

Check that the symlink was created correctly. Run ls -la /usr/local/bin/signal-cli and confirm it points to /opt/signal-cli-0.14.3/bin/signal-cli.

If the tar command failed, confirm the downloaded file exists by running ls signal-cli-0.14.3-Linux-native.tar.gz in your current directory.

help me fix signal-cli installation on Ubuntu

Link signal-cli to your Signal account

Instead of registering a new phone number, signal-cli will link to your existing Signal account as a secondary device. This is the same mechanism Signal Desktop uses.

What is a linked device?

Your phone remains the primary device. signal-cli joins as a secondary device (like Signal Desktop). Messages sent to or from the linked device appear on all your devices.

No separate phone number or registration is needed. You just scan a QR code from your phone to approve the link.

  • Start the linking process by running:
signal-cli link -n "hermes-agent"

What does this command do?

The link command tells signal-cli to request a new device pairing with your Signal account. The -n "hermes-agent" flag sets the display name for this linked device so you can identify it later.

Since v0.14.0, this command prints a scannable QR code directly in your terminal. Do not close or interrupt this process until linking is complete.

  • On your phone, open the Signal app.
  • Tap your profile icon to open Settings.
  • Tap Linked Devices.
  • Tap Link New Device (or the + button).
  • Scan the QR code displayed in your terminal.

After scanning, the terminal process completes automatically. This confirms the link was successful.

  • Sync your contacts to the newly linked device by running:
signal-cli -u [[SIGNAL_PHONE_NUMBER="your phone number in +1234567890 format"]] receive

What does this command do?

The receive command pulls your contact list and group memberships from your phone to the newly linked device. This is required after every fresh link.

Your signal-cli session data is now stored in ~/.local/share/signal-cli/. Treat this directory like a password. You will lock it down with strict permissions in the next step.

  • Confirm the link by checking your Signal app. You should see a device named hermes-agent in the Linked Devices list.

QR code not appearing?

The QR code only prints when your terminal is interactive (not piped). Make sure you are running the command directly in your terminal, not inside a script.

If the link times out before you can scan, run the link command again. The link URI expires after a short period.

help me link signal-cli to my Signal account

Your messaging bridge is in place. Next, you will lock down your Hermes Agent's credentials and Docker backend to ensure the agent cannot leak secrets.

Harden Credentials and Docker Backend

Your Hermes Agent has full terminal access and an AI model generating commands. If credentials are stored insecurely or file protections are missing, the agent could accidentally leak API keys into chat logs or read sensitive files it should never touch.

In this step, you'll confirm the Docker backend is active, lock down your credential file, and verify that Hermes's built-in security layers are protecting your secrets.

In this step, get ready to:
  • Verify the Docker backend and Anthropic provider are working.
  • Lock down credential file permissions.
  • Confirm file-safety protections block access to sensitive paths.
Verify Docker backend and Anthropic provider

Hermes supports multiple terminal backends. The Docker backend runs every command inside an isolated container, which means even if the agent runs something destructive, your host system stays untouched.

  • Confirm the Docker backend is set by running this command:
hermes config set terminal.backend docker

What does this command do?

This tells Hermes to route all terminal commands through a Docker container. If it was already set to docker, the command simply confirms the existing value.

When using the Docker backend, Hermes applies strict security hardening to every container it spins up. This is the primary security boundary for your agent.

  • Run the diagnostic command to validate your full configuration:
hermes doctor

You should see green checkmarks across the diagnostic sections, including a passing Anthropic provider check.

What does hermes doctor check?

This command validates your config files, checks API key resolution, tests provider connectivity, and confirms tool availability. It also verifies that your Claude Code credentials are auto-discovered from ~/.claude.json.

Hermes natively supports Anthropic as a first-class provider and auto-discovers Claude Code credentials. You do not need to manually paste an API key.

Seeing a red X on the Anthropic check?

Make sure Claude Code is still authenticated. Run claude in a separate terminal to confirm it starts a session. If it prompts you to log in, complete the authentication and then run hermes doctor again.

Help me fix my Anthropic provider check in hermes doctor

Lock down your credential file

Hermes stores API keys and secrets in ~/.hermes/.env. By default, other users on the system might be able to read this file. You need to restrict access to your user account only.

  • Set strict file permissions on your credential file by running:
chmod 600 ~/.hermes/.env

What does chmod 600 do?

The 600 permission means only the file owner can read or write the file. No other user or group on the system can access it. This is the recommended permission level for any file containing secrets.

  • Verify the permissions took effect by running:
ls -la ~/.hermes/.env

You should see -rw------- at the start of the line. This confirms only your user has read/write access.

  • Confirm that secrets live in .env and not in config.yaml by checking your config file:
cat ~/.hermes/config.yaml

Scan the output for any API key values. You should see configuration settings like terminal.backend and model.provider, but no actual secret strings. All secrets belong in .env only.

Why separate .env from config.yaml?

Hermes uses config.yaml for non-secret settings and .env for secrets. This separation means you could safely share or back up your config.yaml without risk of leaking credentials.

Credential redaction is also enabled by default since v0.13.0. This means Hermes automatically strips secret-like patterns (API keys, tokens, JWTs) from chat output and logs, preventing accidental leaks into your Signal messages.

See API keys in config.yaml?

If you find secret values in config.yaml, move them to ~/.hermes/.env instead. Use the format KEY_NAME=value (one per line) and remove the key from config.yaml. Then run hermes doctor to confirm everything still resolves.

Help me move secrets from config.yaml to .env

Verify file-safety protections

Even with redaction enabled, the safest approach is preventing the agent from reading credential files in the first place. Hermes includes file-safety rules that block read_file access to sensitive directories.

  • Start a quick Hermes chat session to test the file-safety boundary:
hermes
  • Once the chat starts, ask Hermes to read a protected credential path. Type something like:
Read the file ~/.ssh/id_rsa and show me its contents

Hermes should refuse this request. The file-safety system blocks read access to credential paths including ~/.ssh, ~/.hermes/.env, ~/.aws, and other sensitive directories.

What paths does Hermes block?

The file-safety system blocks agent access to SSH keys, your active .env file, shell history, and credential directories like ~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, ~/.docker, ~/.azure, and ~/.config/gh.

The credential pool also resolves keys from environment variables rather than plaintext files. This means the agent never holds the raw credential value in its context.

  • Exit the Hermes chat by pressing Ctrl+C or typing /exit.

Hermes successfully read the file?

If the agent returned file contents instead of refusing, make sure you are running a recent version of Hermes. Run hermes update to pull the latest version, which includes the file-safety protections. Then try the test again.

Why is Hermes reading my credential files when it should be blocked?

Your credentials are now properly isolated. The Docker backend contains any agent actions, file permissions block unauthorized access, and the redaction system catches any secrets that slip through. Next up, you'll configure the command approval system that gates dangerous operations.

Configure Command Approval and Dangerous-Command Detection

Your Hermes Agent is running inside a Docker container with hardened credentials. But container isolation alone is not enough.

An AI agent with terminal access can still run destructive commands inside its sandbox. The command approval system adds a human-in-the-loop checkpoint. Before any dangerous command executes, Hermes pauses and asks for your explicit permission.

In this step, get ready to:
  • Confirm the approval mode is set to prompt for all dangerous commands.
  • Test the approval system by triggering a dangerous command and verifying denial works.
  • Understand how container isolation interacts with the approval system.
Review the approval mode in your config

Hermes checks every shell command against a curated list of dangerous patterns before executing it. The behavior when a match is found depends on the approvals.mode setting in your config file.

  • Confirm the current approval mode by running this command:
cat ~/.hermes/config.yaml | grep -A 2 approvals

What does this show you?

You should see approvals.mode set to on. This is the default and means Hermes always prompts you before running a dangerous command.

Never set this to auto for a gateway-connected agent. When Hermes is reachable via Signal, you want every destructive action gated behind your explicit approval.

What counts as a dangerous command?

Hermes flags commands matching these categories:

  • Recursive deletes (rm -rf).
  • Anything touching system config directories (/etc or /root).
  • Commands requiring elevated privileges (sudo).
  • Network exfiltration patterns (curl | sh).
  • Force pushes (git push --force).
Test the approval system from the CLI

Seeing the approval prompt in action builds confidence that it actually works. You will start an interactive Hermes session and ask it to do something destructive.

  • Start an interactive Hermes session by running:
hermes

What happens when you run this?

Hermes opens an interactive chat session in your terminal. You will see a welcome banner showing your model, terminal backend (Docker), and available tools. You can type messages directly to the agent.

  • Once the session starts, type this message and press Enter:
delete all files in /tmp

Hermes will attempt to run a recursive delete command. Because this matches the dangerous-command pattern list, you should see an approval prompt appear before anything executes.

What does the approval prompt look like?

The prompt shows the flagged command, a description of why it was caught, and four response options:

  • once allows this single execution only.
  • session allows this pattern for the rest of the session.
  • always adds the command to a permanent allowlist.
  • deny (default) blocks the command entirely.
  • Choose to deny the command when the prompt appears.
  • Verify that the command was blocked and Hermes did not delete anything.

The denial confirms that the fail-closed behavior works. If you had not responded at all, the command would also be denied after the configured timeout.

What is fail-closed?

Fail-closed means that when no response is given within the timeout period, the system defaults to denial. This is critical for a gateway-connected agent.

If your phone loses connectivity while the agent is waiting for approval, the dangerous command is automatically blocked rather than automatically allowed.

  • Exit the Hermes session by pressing Ctrl+D.

Not seeing the approval prompt?

Make sure you are not running Hermes with the --yolo flag. This flag bypasses all dangerous-command approval prompts.

Check that approvals.mode in ~/.hermes/config.yaml is set to on, not off.

Help me debug why Hermes is not showing the dangerous-command approval prompt

Understand the container bypass behavior

Your Hermes Agent uses a Docker backend, which means every command runs inside an isolated container. This raises an important question about how the approval system interacts with container isolation.

Why do dangerous-command checks still appear in Docker mode?

When you connect Hermes to Signal in the next step, the approval system sends dangerous-command details to your Signal chat and waits for your reply. This gives you visibility into what the agent is doing, even though the container already limits the blast radius.

Container isolation is the primary security boundary. The approval system is defense-in-depth. It catches cooperative-mode mistakes (the agent trying something you did not intend) while the container ensures that even if something slips through, it cannot touch your host filesystem.

This layered approach means you get both: human oversight of the agent's actions via Signal, and OS-level containment as the hard boundary.

What is defense-in-depth?

Defense-in-depth is a security strategy that uses multiple layers of protection. No single layer is expected to be perfect. If one layer fails, the next layer catches the threat.

In your setup: user authorization stops strangers, command approval stops accidents, and container isolation stops everything else.

Your approval system is confirmed and tested. Next up, you will wire this entire secured agent to Signal so you can interact with it from your phone.

Set Up the Signal Gateway with User Authorization

Your Hermes Agent is now secured at the application layer. The approval system catches dangerous commands, credentials are redacted, and Docker isolates everything from your host.

The next piece is the communication channel. Right now, you can only interact with Hermes from the same terminal. To make the agent truly useful, you need to message it from anywhere. That means wiring up a Signal gateway with strict access control so only your phone number can talk to the agent.

In this step, get ready to:
  • Start signal-cli as an HTTP daemon on localhost and lock down its session data.
  • Configure Hermes with Signal environment variables and a strict user allowlist.
  • Test the gateway end-to-end from your phone and verify unauthorized users are rejected.
Start the signal-cli daemon in HTTP mode

Hermes communicates with signal-cli over a local HTTP interface. You need to start signal-cli as a background daemon that listens on localhost only, ensuring no external network access to the Signal bridge.

  • Start signal-cli in daemon mode with the HTTP interface by running this command:
signal-cli --account [[SIGNAL_PHONE="your phone number in +1234567890 format"]] daemon --http 127.0.0.1:8080 &

What does this command do?

The --account flag tells signal-cli which linked account to use. The daemon --http 127.0.0.1:8080 part starts signal-cli as a persistent process exposing a JSON-RPC HTTP endpoint.

Binding to 127.0.0.1 (not 0.0.0.0) is critical. It means only processes on your local machine can reach signal-cli. No external device on your network can connect to it directly.

The & at the end backgrounds the process so you can continue using your terminal.

  • Wait a few seconds for the daemon to initialize, then verify it is running by curling the health check endpoint:
curl http://127.0.0.1:8080/api/v1/check
  • You should see an HTTP 200 response. This confirms signal-cli is listening and ready to process requests.
  • Lock down the signal-cli session data directory by setting strict permissions:
chmod 700 ~/.local/share/signal-cli/

Why lock down this directory?

The ~/.local/share/signal-cli/ directory contains your Signal session credentials. Anyone with read access to this folder can impersonate your Signal account. Setting permissions to 700 ensures only your user can read, write, or list its contents.

  • Confirm the permissions were applied by running:
ls -ld ~/.local/share/signal-cli/
  • You should see drwx------ at the start of the output, confirming owner-only access.

Curl returning connection refused?

If curl reports a connection error, signal-cli may still be starting up. Wait 5-10 seconds and try again.

If it still fails, check if the process is running with jobs or ps aux | grep signal-cli. Look for error output in your terminal. The most common issue is specifying the wrong account phone number.

Ask for help: Why is my signal-cli daemon not starting on port 8080?

Configure Hermes for Signal

With signal-cli running, you now need to tell Hermes where to find it and who is allowed to send messages. This is configured through environment variables in your ~/.hermes/.env file.

  • Open ~/.hermes/.env in your text editor and add the following three environment variables at the end of the file:
# Signal gateway configuration
SIGNAL_HTTP_URL=http://127.0.0.1:8080
SIGNAL_ACCOUNT=[[SIGNAL_PHONE="your phone number in +1234567890 format"]]
SIGNAL_ALLOWED_USERS=[[SIGNAL_PHONE="your phone number in +1234567890 format"]]

What do these variables control?

SIGNAL_HTTP_URL tells Hermes where to reach the signal-cli HTTP daemon. It must match the address you used when starting the daemon.

SIGNAL_ACCOUNT identifies which Signal account the bot uses. This must be in E.164 format (country code + number, like +14155551234).

SIGNAL_ALLOWED_USERS is your security allowlist. Only phone numbers listed here can interact with the agent. Set it to your own number only. This is the first line of defense against unauthorized access.

Do NOT set SIGNAL_ALLOW_ALL_USERS

The SIGNAL_ALLOW_ALL_USERS variable defaults to false. Never set it to true for a gateway-connected agent. Doing so would let anyone who knows your Signal number send commands to your AI agent, including dangerous ones.

  • Save ~/.hermes/.env.
  • Verify the file still has strict permissions:
ls -la ~/.hermes/.env
  • Confirm the output shows -rw------- (permissions 600). If it shows something else, re-apply with chmod 600 ~/.hermes/.env.

Not sure what E.164 format is?

E.164 is the international phone number format. It starts with a + followed by the country code and number with no spaces or dashes. For example, a US number would be +14155551234. A UK number would be +447911123456.

Ask for help: How do I convert my phone number to E.164 format?

Start the gateway and test authorization

Everything is wired up. Time to start the Hermes gateway and confirm it responds to your messages while rejecting everyone else.

  • Start the Hermes gateway in the foreground by running:
hermes gateway

What does hermes gateway do?

This command starts Hermes in gateway mode. It connects to the signal-cli daemon, listens for incoming messages on your Signal account, and routes them to the AI agent. The agent's responses are sent back through the same channel.

Running in the foreground lets you see real-time logs of incoming messages, approvals, and any errors.

  • Pick up your phone and open the Signal app.
  • Send a message to yourself (your own number). Type something simple like "hello" or "what time is it".
  • Watch your terminal. You should see Hermes log the incoming message, process it, and send a response.
  • Check your phone. You should receive a reply from the agent in your Signal chat within a few seconds.

How does authorization work?

Hermes uses a three-tier access control flow for Signal:

  • If SIGNAL_ALLOWED_USERS is set, only those phone numbers can interact with the agent. Everyone else is silently ignored.
  • If no allowlist is set, unknown users receive a DM pairing code they must present to you for approval (using hermes pairing approve signal CODE).
  • If SIGNAL_ALLOW_ALL_USERS=true, anyone can message the agent with no restrictions.

Since you set SIGNAL_ALLOWED_USERS to your own number, the gateway defaults to deny for everyone else. Unconfigured users who attempt to message will enter the DM pairing flow and require your explicit approval.

  • To verify authorization is working, have a friend send a message to your Signal number (or use a second Signal account if you have one). They should receive no response from the agent. In your gateway logs, you should see the message being rejected or triggering a pairing code.

Agent not responding to your messages?

Double-check that the phone number in SIGNAL_ACCOUNT and SIGNAL_ALLOWED_USERS exactly matches your number in E.164 format. Even a missing + prefix will cause a mismatch.

Make sure signal-cli is still running. Check with curl http://127.0.0.1:8080/api/v1/check. If it has stopped, restart the daemon command from the first substep.

Ask for help: Why is my Hermes gateway not responding to Signal messages?

Your Signal gateway is live and locked down. Only your phone number can interact with the agent, and every message travels over Signal's end-to-end encrypted channel. Next up, you'll put all of these security layers to the test with a full end-to-end verification.

Test Your Security Boundaries End-to-End

Your Hermes Agent now has every security layer configured and running. The gateway responds to your phone, credentials are locked down, and the Docker backend contains any agent actions.

Configuration alone does not prove security. In this step, you'll actively probe each boundary to confirm it holds under real conditions.

In this step, get ready to:
  • Test the dangerous command approval system via Signal.
  • Verify credential redaction in agent responses.
  • Confirm Docker container isolation prevents host filesystem access.
Test the dangerous command approval system

The approval system is your human-in-the-loop gate. Before Hermes executes anything flagged as dangerous, it pauses and asks for your explicit consent via Signal.

  • Open Signal on your phone.
  • Send this message to your Hermes conversation:
run sudo apt update

Why this message?

The sudo prefix matches Hermes's dangerous-pattern list. This triggers the approval gate before execution.

Within a few seconds, Hermes sends back a message in your Signal chat showing the command it wants to run and instructions to reply with /approve or /deny.

  • Reply with /approve to allow the command this one time.
  • Hermes executes the command inside the Docker container and sends you the output.

What are /approve and /deny?

These are gateway slash commands. When a dangerous command is pending, you reply /approve to allow it or /deny to block it. Bare text like "yes" or "ok" does not trigger approval.

The /approve command also supports scoping: /approve session approves the pattern for the current session, and /approve always approves it permanently.

Now test the fail-closed timeout behavior.

  • Send another dangerous command via Signal:
delete everything in /tmp
  • This time, do NOT reply. Wait for the approval to expire (5 minutes).
  • After the timeout, Hermes sends a message confirming the command was denied.

Why fail-closed matters

If no response is given within the timeout window, the command is denied by default. This is called fail-closed behavior. It means an unattended agent cannot accidentally execute something destructive while you're away from your phone.

Don't see an approval prompt?

Check that approvals.mode in ~/.hermes/config.yaml is set to manual (the default). If it's set to off, all approval checks are disabled.

Also confirm your gateway is still running. Switch to your Ubuntu terminal and check for active gateway output.

Help me troubleshoot why my Hermes agent isn't showing approval prompts for dangerous commands via Signal.

Verify credential redaction is active

Hermes strips secret-like patterns from all display output before sending responses. This prevents API keys from leaking into your Signal chat logs.

  • In Signal, send this message to Hermes:
show me all environment variables
  • Look at the response Hermes sends back. Any API keys or tokens are replaced with redacted placeholders (asterisks or similar masking).

Now test the file-safety protections that block direct reads of credential paths.

  • Send this message to Hermes via Signal:
read the contents of ~/.hermes/.env
  • Hermes should refuse or return a blocked-access message. The file-safety layer blocks read_file access to credential paths including ~/.hermes/.env, ~/.ssh, ~/.aws, and other sensitive directories.

How does redaction work?

Hermes strips secret-like patterns from all display output before it reaches you. This is enabled by default since v0.13.0. Even if the agent reads an environment variable containing a key inside its container, the value is masked before the response is sent through Signal.

The file-safety layer adds a second defense. It blocks the agent's read_file tool from accessing known credential locations entirely.

Seeing full API keys in the response?

Redaction is enabled by default. If you see unmasked keys, verify you're running Hermes v0.13.0 or later. Run hermes version in your terminal to check.

My Hermes agent is showing full API keys in Signal responses instead of redacting them. Help me fix this.

Confirm container isolation

The Docker backend is Hermes's primary security boundary. Every command the agent runs executes inside an isolated container, not on your host machine. You'll prove this by creating a file through the agent and showing it exists only inside the container.

  • In Signal, send this message to Hermes:
create a file at /tmp/security-test.txt with the content 'hello from hermes'
  • Wait for Hermes to confirm it created the file.

Now verify the file does NOT exist on your host filesystem.

  • Switch to your Ubuntu terminal and check for the file by running:
cat /tmp/security-test.txt
  • You should see an error like No such file or directory. The file does not exist on your host because the agent ran the command inside the Docker container.

Finally, confirm the file DOES exist inside the container.

  • In Signal, send this message to Hermes:
read the contents of /tmp/security-test.txt
  • Hermes responds with the file contents: hello from hermes. This proves the file exists inside the container's isolated filesystem.

Why container isolation is the primary boundary

OS-level isolation is the real security boundary, not application-level checks. Even if every other layer failed, the Docker container prevents agent actions from touching your host system. Dangerous command approval, redaction, and file-safety are defense-in-depth layers on top of this foundation.

File exists on your host?

This means your terminal backend is not set to Docker. Verify by running hermes config set terminal.backend docker and restarting the gateway. The agent must be configured to use the Docker backend for container isolation to work.

The file I asked Hermes to create appeared on my host filesystem instead of being isolated in the Docker container. Help me fix my terminal backend configuration.

Secret mission

Persistent Service with Firewall Lockdown

Your agent works, but it dies when you close the terminal or reboot the machine. In production, an always-on assistant needs to survive restarts automatically and reject all unsolicited network traffic. Can you wire up systemd services and a UFW firewall to make your agent truly production-grade?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally and uses your existing Claude Code subscription for inference, so there are no additional ongoing costs.

Resources you used:

  • signal-cli v0.14.3 installation (/opt/signal-cli-0.14.3 and symlink at /usr/local/bin/signal-cli).
  • signal-cli linked device on your Signal account.
  • signal-cli session data (~/.local/share/signal-cli/).
  • signal-cli systemd user service (Secret Mission).
  • Hermes gateway system service.
  • UFW firewall rules (Secret Mission).

Keep everything running

No action needed. Choose this if you want an always-on AI agent you can message from your phone at any time.

  • The signal-cli systemd user service and Hermes gateway system service continue running in the background.
  • The only cost is usage against your existing Claude Code subscription each time you send the agent a message.
  • Java 25 (Adoptium Temurin) remains installed on your system. It does not consume resources when idle.

Pause - I'll come back to this later

Stop the gateway and signal-cli processes to free up memory. Your configuration files and linked device stay intact so you can restart later.

  • Stop the Hermes gateway by running:
  • Stop the signal-cli daemon by running:
hermes gateway stop
systemctl --user stop signal-cli

To restart later, run systemctl --user start signal-cli followed by hermes gateway.

Delete - I don't want to use this again

Remove all project resources completely. This unlinks your Signal device, removes signal-cli, and tears down the services.

  • Stop and disable the signal-cli systemd user service (if you completed the Secret Mission):
systemctl --user stop signal-cli
systemctl --user disable signal-cli
  • Uninstall the Hermes gateway system service:
hermes gateway uninstall
  • Reset UFW firewall rules to defaults (if you completed the Secret Mission):
sudo ufw reset
  • Remove the signal-cli installation and symlink:
sudo rm -rf /opt/signal-cli-0.14.3 /usr/local/bin/signal-cli
  • Remove signal-cli session data:
rm -rf ~/.local/share/signal-cli/
  • Remove the Signal-specific environment variables from ~/.hermes/.env. Open the file and delete the lines containing SIGNAL_HTTP_URL, SIGNAL_ACCOUNT, and SIGNAL_ALLOWED_USERS.
  • Unlink the signal-cli device from your phone. Open Signal on your phone, go to Settings then Linked Devices. Tap the linked device and select Remove.

Nice Work!

Nice Work!

Your Hermes Agent is now a hardened, encrypted personal assistant you can securely message from anywhere. You've deployed a production-grade AI agent with seven layers of defense-in-depth security, accessible only through Signal's end-to-end encrypted channel.

Here's what you accomplished:

  • Configured Docker container isolation as the primary security boundary, ensuring no agent action can affect your host system.
  • Enabled a command approval system that gates dangerous operations behind your explicit consent, delivered as prompts in your Signal chat with fail-closed timeout behavior.
  • Deployed a Signal gateway with strict allowlist authorization and credential redaction, so only your phone number can interact with the agent and API keys never appear in chat logs.
  • Secret Mission: Created persistent systemd services and a UFW firewall lockdown so your agent survives reboots and rejects all inbound network traffic.

Ready to quiz yourself?