Your AI Agent Can Read Your SSH Keys

I ran truffleHog on my home directory last week. It found credentials. Old project tokens, mostly. Nothing that mattered. But the scan surfaced the real problem: every process running as my user could read every secret file on the machine. Including the AI agent I was talking to.

This isn't a hypothetical. Hermes Agent, OpenClaw, Claude Code, and every other coding agent run with your full user permissions. They can read ~/.ssh/id_ed25519, ~/.aws/credentials, ~/.password-store/, and every .env file in your projects. Worse: they can be tricked into doing so. A maliciously crafted prompt that asks the agent to "check your SSH config for issues" or "audit your AWS credentials for best practices" is indistinguishable from legitimate use.

The Threat Model Isn't Disk Encryption

Disk encryption (LUKS) protects secrets when the machine is off. It does nothing against a running process that shares your UID. Environment variable managers like direnv keep secrets out of git and shell history, but any process can read /proc/*/environ. Mozilla's SOPS (Secrets OPerationS, encrypts structured files with age/GPG keys) and pass (the standard Unix password manager, stores secrets as GPG-encrypted files) encrypt at rest, but the moment a tool decrypts a secret for use, it's in memory, readable by any same-user process with ptrace or /proc/*/mem.

The actual threat: a same-user process reading files it shouldn't. The solution: kernel-enforced filesystem restrictions via Landlock, a Linux Security Module (since 5.13) that lets unprivileged processes create their own filesystem sandboxes with no root, no daemon, and no config files.

The Layered Defense

There is no single tool that solves this. The right approach is layers, each raising the cost of exfiltration.

Layer 1: Encrypt at rest (SOPS + age)

SOPS encrypts individual values or entire .env files using age keys. The encrypted file is safe to version-control. A stolen laptop or a misconfigured rsync doesn't leak secrets.

sops --encrypt --age age1... .env > secrets.enc.yaml

This protects against disk theft and accidental exposure. It does not protect against a running agent process that asks the user "can you decrypt this config so I can check it?"

Layer 2: Hardware-bound secrets (systemd-creds + TPM)

Most modern x86 CPUs (AMD Ryzen, Intel Core) include a firmware TPM (fTPM). systemd-creds can encrypt credentials so they only decrypt on that specific physical chip and only for a specific systemd service:

[Service]
LoadCredentialEncrypted=openai-key:/etc/credstore.encrypted/hermes/openai-key

The credential lands in a private tmpfs ($CREDENTIALS_DIRECTORY) accessible only to the service's process tree. Other same-user processes cannot reach it. Steal the disk image, and the credential is useless without the TPM. This is the gold standard for daemon secrets, but it only works for systemd services.

Layer 3: Kernel keyrings (keep secrets off disk and out of env vars)

Linux has an in-kernel key management service. Secrets live in kernel memory, never on disk, never in environment strings:

keyctl add user openai-api-key "$(pass show api/openai)" @u
keyctl pipe user openai-api-key | some-tool --api-key @-

No /proc leakage. No env var dump. A compromised process has to explicitly search for each key by name: a higher bar than cat /proc/self/environ. Keys persist until logout.

Layer 4: Landlock (kernel VFS whitelist)

Landlock, merged in Linux 5.13, lets unprivileged processes restrict their own filesystem access. Add a path to the whitelist, and the kernel blocks every other path at the VFS layer: before the file is opened, before symlinks are resolved, before any userspace code runs.

This is the layer that directly addresses the AI agent problem. Apply it, and your agent literally cannot open ~/.ssh/ or ~/.aws/, even if tricked.

Landlock in Practice

I built a 200-line C wrapper that applies Landlock restrictions before exec-ing the agent:

#define RW_ACCESS (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE | ...)

/* Allow the agent to read/write its own config and your project directories */
add_path_rule(ruleset_fd, "/home/you/.hermes", RW_ACCESS);
add_path_rule(ruleset_fd, "/home/you/projects", RW_ACCESS);
add_path_rule(ruleset_fd, "/usr", RO_ACCESS);
add_path_rule(ruleset_fd, "/etc", RO_ACCESS);
/* ... no entry for ~/.ssh, ~/.aws, ~/.password-store ... */

landlock_restrict_self(ruleset_fd, 0);
execvp("hermes", argv);

The whitelist is explicit: ~/.hermes (config, state, sessions), ~/projects (working directory), and the system paths the agent needs to run (/usr, /etc, /proc, /dev, /tmp). Everything else is denied by the kernel. After enforcing:

$ ./landlock-hermes ls ~/.ssh/
ls: cannot open directory '/home/you/.ssh/': Permission denied

$ ./landlock-hermes ls ~/.aws/
ls: cannot open directory '/home/you/.aws/': Permission denied

$ ./landlock-hermes ls ~/.password-store/
ls: cannot open directory '/home/you/.password-store/': Permission denied

Same user. Same UID. The kernel says no.

What Landlock Stops (and What It Doesn't)

Symlinks don't bypass it. Landlock operates on inodes, not paths. If ~/projects/evil-link -> ~/.ssh/id_ed25519, the kernel resolves the symlink to the real file's inode, checks the Landlock ruleset, finds no rule covering that inode, and blocks access.

/proc walks don't bypass it. Even if /proc is readable (required for basic process operation), the file descriptors and memory maps exposed there only reference files the process can actually reach. If the agent can't open ~/.ssh/id_ed25519, it won't find it in /proc/self/fd/.

ptrace still works. A same-user process with CAP_SYS_PTRACE or ptrace_scope=0 can still attach to the agent and read its memory. If the agent loaded a secret into memory before the Landlock ruleset was applied, ptrace can extract it. This is an unsolved problem on stock Linux without a full namespace sandbox.

Environment variables are still readable. Landlock restricts filesystem access, not /proc/*/environ. If you pass secrets to the agent via environment variables, they remain visible. Use kernel keyrings or systemd-creds for those.

Gotchas

SSH-based Git breaks. Git over SSH needs ~/.ssh/. Either switch repos to HTTPS with tokens, or accept that git operations fail inside the sandbox and stage them outside.

pass breaks. The password store CLI needs ~/.password-store/. This is intentional; the whole point is preventing the agent from reading your password store. If the agent genuinely needs a credential, pass it through a kernel keyring pipe or a systemd credential.

Package managers may need more paths. If the agent installs Python packages at runtime, it needs write access to site-packages under /usr/lib/python3.*/ or a venv inside the whitelist. Same for npm, cargo, etc.

The whitelist must be maintained. When the agent needs access to a new directory, you update the C wrapper and recompile. This is a feature, not a bug: every expansion of access is explicit and intentional.

Landlock's Gaps

Landlock only restricts filesystem access. A complete agent sandbox needs additional controls:

  • Network egress filtering. Landlock can restrict TCP bind/connect as of Linux 6.8, but the wrapper above doesn't use it yet. An agent should not be able to exfiltrate data to arbitrary hosts even if it reads a secret from memory.
  • Bubblewrap / Firejail for namespace isolation. Combine Landlock with a PID/mount namespace so the agent sees an entirely different filesystem tree. This closes the ptrace gap.
  • Seccomp for system call filtering. Block ptrace, process_vm_readv, and other same-user introspection calls.

The First-Class Solution: Container Isolation

The Landlock approach works, but it's maintenance-heavy and leaves gaps. If you're running Hermes, there's a better answer: the Docker terminal backend. Instead of a kernel VFS whitelist that you have to hand-tune per project, you run the agent inside a container with explicit volume mounts. The container sees only the directories you mount. No ~/.ssh, no ~/.aws, no ~/.password-store -- unless you deliberately pass them through.

# ~/.hermes/profiles/secure/config.yaml
terminal:
  backend: docker
  docker_image: "secure-agent:latest"
  docker_forward_env: []         # no host env vars leaked
  container_persistent: true
  docker_volumes:
    - "/home/you/projects:/workspace/projects"
    # ── and nothing else. No ~/.ssh, no ~/.aws, no ~/.config.

The agent has full apt, pip, cargo inside the container. It can compile, install, and run anything. It just cannot reach your secrets. No ptrace gap, no /proc leakage, no whitelist to maintain. If the agent needs Docker itself (to build images or run docker compose), you mount the socket:

  docker_volumes:
    - "/var/run/docker.sock:/var/run/docker.sock"
    - "/home/you/projects:/workspace/projects"

This is the pattern Hermes profiles are designed for. Build a minimal Docker image with your toolchain (a Dockerfile in the profile directory), mount only the project directories the agent needs, and forward zero environment variables. The agent is fully isolated at the container boundary, which is a stronger guarantee than any same-user sandbox.

The Hermes security documentation covers the full stack: secrets redaction, approval gating, command allowlisting, and an incident response workflow for when things go wrong despite all the layers.

Landlock is the native Linux answer. The Docker backend is the Hermes answer. Both work. One requires 200 lines of C and ongoing path maintenance. The other requires a Dockerfile and two lines of config. Pick the one that fits your threat model, but don't run an agent with unfettered access to everything your user can read. That's the one thing all of these approaches exist to prevent.