Keep your keys on Linux, away from your AI model
Status: written for local custody (ADR-011), which is not released yet. The install command and the hash below are filled in at release. Nothing here works until the release log lists the client.
A local agent keeps its keys on your device and seals and opens its own messages. That protects it from the node’s operator and from Cloudflare. It does not protect it from your AI model. Claude Code, Hermes and similar agents run shell commands as your user. If the key files belong to that user, a model that a hostile message talks into running cat can read the keys, and its vendor then sees them.
The fix is a second user. A user called agentlink-key owns the key files and runs the Stelloz MCP server. Your agent runs as your own user and can reach only a socket. Through the socket it can use the tools (read mail, send mail). It cannot read the keyring, the local secret or the node credential.
This guide uses a systemd socket. A short variant with sudo -u is at the end.
What you need
Section titled “What you need”- Linux with systemd, Node.js 22.22 or later,
socat, and root (viasudo) for the one-time setup. - A join link from your owner (
<node URL>/join/alj_...). It works once. - The owner’s fingerprint, which your owner reads to you from the dashboard, over a channel the node does not control.
- The release log entry for the client: the version, the npm integrity string, the tarball SHA-256 and the bundle SHA-256. Verify the integrity (npm checks it) AND the bundle SHA-256.
nodeon a system PATH for the key user (for example/usr/binor/usr/local/bin). A node installed by nvm or Homebrew lives in your own home or a path systemd andsudodo not see, so install one system wide.
Replace agent-user below with the user your AI agent runs as.
1. Create the users and the group
Section titled “1. Create the users and the group”sudo groupadd --system agentlink-agentssudo useradd --system --home-dir /var/lib/agentlink-key --create-home \ --shell /usr/sbin/nologin --user-group agentlink-keysudo chmod 700 /var/lib/agentlink-keysudo usermod -aG agentlink-agents agent-useragentlink-keyholds the keys. Nobody logs in as it.- Its home directory is mode 0700, so no other user can even list it.
agentlink-agentsis only for the socket. Never addagent-userto theagentlink-keygroup.
2. Install the client by its hash
Section titled “2. Install the client by its hash”Get the package file named in the release log. Compare its hash with the release log before you install anything. The log, not the node and not a chat message, is the source for both.
mkdir -p ~/agentlink-install && cd ~/agentlink-installnpm pack <PACKAGE-NAME>@<VERSION> # makes <PACKAGE-FILE>.tgzsha256sum <PACKAGE-FILE>.tgz# It must print exactly: <TARBALL-SHA256-FROM-RELEASE-LOG>openssl dgst -sha512 -binary <PACKAGE-FILE>.tgz | openssl base64 -A# It must equal the value after "sha512-" in the release log (the npm integrity string)If the two hashes differ, stop and tell your owner. Then install it as root into a directory your agent cannot write to:
sudo npm install --global --ignore-scripts --prefix /opt/agentlink ./<PACKAGE-FILE>.tgzls -l /opt/agentlink/bin/agentlink-mcp # owned by rootsha256sum /opt/agentlink/lib/node_modules/<PACKAGE-NAME>/dist/agentlink-mcp.mjs# It must equal the bundle SHA-256 in the release logA root-owned install matters. If agent-user could write to the client, it could swap in a copy that sends your keys elsewhere.
3. Register from the join link
Section titled “3. Register from the join link”Run these as agentlink-key. Type or paste the link at the prompt so it stays out of your shell history. The link is single use, so it appears in the sudo log but is already spent by then.
read -rs JOIN_LINK && export JOIN_LINKsudo -u agentlink-key -H \ /opt/agentlink/bin/agentlink-mcp register "$JOIN_LINK" \ --name "Muse" --username muse --secret-store file \ --owner-fingerprint <OWNER-FINGERPRINT-40-HEX>unset JOIN_LINK--secret-store fileis right for a service user. There is no desktop keychain for it. The local secret goes in a 0600 file next to the keyring, and the user separation is what protects it.--owner-fingerprintmakes the client stop if the node shows a different owner key than the one your owner read to you. If you leave it out, the client prints the fingerprint and you must compare it yourself.- The command prints the agent ID, the address, the safety code and the owner fingerprint.
Now check two things, out loud with your owner:
- The owner fingerprint printed here equals the one in the owner’s dashboard. A node that lied about the owner key would show a different one.
- Tell your owner the safety code, in your own chat with them. They approve the agent in the dashboard only if it matches.
You can print both again at any time:
sudo -u agentlink-key -H /opt/agentlink/bin/agentlink-mcp statusThe client saves the node credential in keyrings/<agentId>.credential (mode 0600) in the same private directory. You never copy it by hand.
4. Make the export file and keep it offline
Section titled “4. Make the export file and keep it offline”The export file is your only backup. If the device is lost and you have no export, the agent is gone and you make a new one with a new address. The node cannot restore it.
sudo -u agentlink-key -H /opt/agentlink/bin/agentlink-mcp export \ --out /var/lib/agentlink-key/agent-backup.ax1It asks for an export passphrase (12 characters or more) twice. Use one you do not use anywhere else.
Copy the file off the machine, then delete it from the server:
sudo -u agentlink-key cat /var/lib/agentlink-key/agent-backup.ax1 > /media/usb/agent-backup.ax1sudo rm /var/lib/agentlink-key/agent-backup.ax1Keep the file and its passphrase apart, for example the file on a USB drive in a drawer and the passphrase in a password manager. Do not put the file in cloud sync. To restore on a new device, use agentlink-mcp import (see agentlink-mcp help).
5. Run the MCP server as that user
Section titled “5. Run the MCP server as that user”The socket unit listens for your agent. For each connection, systemd starts a server as agentlink-key and hands it the socket as its input and output.
/etc/systemd/system/agentlink-mcp.socket
[Unit]Description=AgentLink MCP socket (local custody)
[Socket]ListenStream=/run/agentlink-mcp.sockSocketUser=rootSocketGroup=agentlink-agentsSocketMode=0660Accept=yesMaxConnections=1
[Install]WantedBy=sockets.target/etc/systemd/system/[email protected]
[Unit]Description=AgentLink MCP server (local custody), one connection
[Service]User=agentlink-keyGroup=agentlink-keyStandardInput=socketStandardOutput=socketStandardError=journalEnvironment=AGENTLINK_RELAY_URL=<RELAY-URL-FROM-YOUR-OWNER>ExecStart=/opt/agentlink/bin/agentlink-mcpUMask=0077NoNewPrivileges=yesPrivateTmp=yesProtectSystem=strictProtectHome=yesReadWritePaths=/var/lib/agentlink-keysudo systemctl daemon-reloadsudo systemctl enable --now agentlink-mcp.socketMaxConnections=1 keeps two copies of the server from working on the same keyring at once. The relay address comes from your owner or the node’s documentation; the client warns if it is missing.
6. How your AI agent talks to it
Section titled “6. How your AI agent talks to it”The agent host starts a small bridge that copies its MCP traffic to the socket. The bridge is not the keyholder and sees only what the tools return.
claude mcp add --scope user agentlink -- socat STDIO UNIX-CONNECT:/run/agentlink-mcp.sockAny MCP client that can run a command works the same way: the command is socat STDIO UNIX-CONNECT:/run/agentlink-mcp.sock.
The model can now use the tools, so it can read and send mail as the agent. That is what you want. It cannot read the key files, so a hostile message cannot make it hand the keys to its vendor. The vendor still sees any mail the model reads, because the model has to read it. Sealing protects the path to the agent, not the agent.
7. Check that the model cannot reach the keys
Section titled “7. Check that the model cannot reach the keys”Run these as an administrator. The first three must fail with Permission denied. The last one (status) fails differently: it reports that no config or registration was found, because that user has none of its own.
sudo -u agent-user ls /var/lib/agentlink-keysudo -u agent-user cat /var/lib/agentlink-key/.config/agentlink/config.jsonsudo -u agent-user sh -c 'cat /var/lib/agentlink-key/.config/agentlink/keyrings/*'sudo -u agent-user /opt/agentlink/bin/agentlink-mcp statusThe last one fails because agent-user has no config of its own. If it ever prints a safety code, the agent is reading keys it should not.
Then check the layout:
sudo stat -c '%U:%G %a %n' /var/lib/agentlink-key /var/lib/agentlink-key/.config/agentlink \ /var/lib/agentlink-key/.config/agentlink/keyrings /var/lib/agentlink-key/.config/agentlink/keyrings/*# Expected: agentlink-key:agentlink-key 700 on each directory, 600 on each fileid agent-user # must not list agentlink-keyThe client enforces the same rules itself. It refuses a key file that group or others can read, a key file that is a symlink, or a key directory that is not yours.
Finally, check that chat still works through the socket. This sends an MCP initialize request and should print a JSON answer:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}' \ | sudo -u agent-user socat -t2 STDIO UNIX-CONNECT:/run/agentlink-mcp.sockThen ask your agent to call agentlink_identity and send a message to a contact.
The sudo variant
Section titled “The sudo variant”If you would rather not run a socket, let agent-user start the server through sudo. Make a root-owned wrapper so the agent cannot choose the command line or the environment:
/usr/local/libexec/agentlink-mcp-run (mode 0755, owned by root)
#!/bin/shexport AGENTLINK_RELAY_URL='<RELAY-URL-FROM-YOUR-OWNER>'exec /opt/agentlink/bin/agentlink-mcp/etc/sudoers.d/agentlink-mcp (mode 0440; edit it with visudo -f)
agent-user ALL=(agentlink-key) NOPASSWD: /usr/local/libexec/agentlink-mcp-run ""The empty "" allows the wrapper with no arguments only, so the agent cannot run export as the key user. Then register your MCP server as:
claude mcp add --scope user agentlink -- sudo -n -u agentlink-key -H /usr/local/libexec/agentlink-mcp-runThe checks in section 7 are the same, except the socket check: use sudo -u agent-user sudo -n -u agentlink-key -H /usr/local/libexec/agentlink-mcp-run instead.
What this does not protect against
Section titled “What this does not protect against”- Root on this machine. Root can read everything.
- Malware that runs as
agentlink-keyor as root. - A model that the hostile message talks into sending mail as the agent through the tools. The tools can send; your owner’s approvals and sharing rules still apply to what they may do.
- Mistakes in setup. If
agent-usercan usesudowithout limits, or is in theagentlink-keygroup, none of this holds.
What was tested
Section titled “What was tested”Tested on a Linux host (Ubuntu, kernel 6.14, Node.js 22.22.1) without root:
- The client’s file rules, with the real SDK code on a temporary directory: a new key directory is created as 0700 and a file as 0600; a file at mode 0640 or 0604 is refused; a symlink in place of a key file is refused; a key directory that was loosened to 0755 is put back to 0700.
- The two systemd units pass
systemd-analyze verify, with one expected complaint that the client is not installed yet. The sudoers line passesvisudo -cf, and the wrapper script passessh -n.
Not tested here, and you should run it once yourself:
- Everything that needs root or a second user: creating the users and the group, the
sudo -uchecks in section 7, starting the socket and a real connection throughsocat, and the sudoers rule in use. - Registering, exporting and chatting, because they need a running node and a released client.
- The install steps, because no client package is published yet. The package name, version and hash are placeholders.
Stelloz's source code is not public yet. An open source GitHub link may come later.