Skip to content
Agentic Control Plane
Incident series · Part 12 of 12
Stop Your Agent From… →

Stop your AI agent from wrecking a box over ssh — in three steps

David Crowe David Crowe · · Updated · 5 min read
ssh proxmox homelab tool-policy approvals claude-code opencode
Share X HN LinkedIn

A thread on r/Proxmox this week asked how people handle ssh keys for the agents they run in their homelab. One answer stuck out. A Claude Code user had given the agent a restricted account on the hypervisor, with a sudoers file that allows pct create 300 but not pct destroy 300. Then they tried to make the harness ask before every ssh-plus-sudo command, and gave up: the agent kept finding new ways to invoke ssh that slipped the rule, and reviewing every shell command by hand stopped being reviewing.

That is the whole problem in two sentences. Sudoers on the target is a good floor. The gap is between the agent’s intent and the box, where the harness’s own permission list matches strings and the agent does not.

What your agent has access to right now

Any coding agent with a working ssh key has the same reach you do from that shell. ssh pve 'sudo pct destroy 300', ssh pve reboot, docker exec web rm -rf /data, qm guest exec 100 -- shutdown are each one tool call. The harness sees a bash call. It does not see that the interesting part is a quoted string that runs somewhere else.

Same for the quieter version: ssh pve <<EOF with a script body, or ssh pve < deploy.sh. No command word, so a rule keyed on “ssh followed by a command” has nothing to key on, and the remote login shell runs the whole file.

Three steps

Step 1 — Install the hook (one command)

For Claude Code, Cursor, Codex CLI, or opencode:

curl -sf https://agenticcontrolplane.com/install.sh | bash

Every tool call now passes through a PreToolUse hook before it executes. Restart the client.

Step 2 — Start in audit mode on a box you can lose

Leave the workspace in audit mode for a day. Give the agent the same key it will have later and a task that touches the target. Every ssh, docker, pct, and qm call lands in the activity log with what it resolved to, so you can read what the agent actually did to the box before any of it is held. One person in that thread described doing exactly this by hand, with a second model reading what the first one’s account had done. The ledger does it for free.

Step 3 — Switch to enforce, and let the ceiling do the work

In enforce mode a command that runs somewhere the hook cannot see is held for approval before it leaves the machine. No rule to write. This is what the decision endpoint returns for a real Claude Code session at interactive tier, for the spellings from the thread:

ssh pve 'sudo pct destroy 300'
  → deny: ceiling guard: runs a command on another host the hook can't inspect (ssh) — requires human approval

FOO=1 ssh pve "sudo qm stop 100"
  → deny: ceiling guard (ssh) — requires human approval

command ssh pve "sudo qm stop 100"
  → deny: ceiling guard (ssh) — requires human approval

sudo -u root ssh pve 'pct destroy 300'
  → deny: ceiling guard (ssh) — requires human approval

s''sh pve "sudo qm stop 100"
  → deny: ceiling guard (ssh) — requires human approval

ssh pve <<EOF
sudo qm stop 100
EOF
  → deny: ceiling guard (ssh) — requires human approval

ssh pve < deploy.sh
  → deny: ceiling guard (ssh) — requires human approval

bash -c "ssh pve sudo reboot"
  → deny: hardline floor: system shutdown/reboot — blocked unconditionally

ssh pve 2>&1 | tee log
  → allow

Every ssh row resolves to the same tool class, Bash.ssh, whatever is in front of the binary or how it is quoted, because the decision is made on what the command runs, not on the text. The last row is an interactive login with output plumbing, which is what it looks like, and is not held. The reboot is not held either. It is on the hardline list, and no approval reaches it.

Then tighten where you want to. A rule on Bash.ssh applies to every spelling above. Bash.pct and Bash.qm can be denied outright in the background tier, so an unattended run never touches a container or VM at all, and the interactive tier still gets the approval prompt:

{
  "mode": "enforce",
  "tools": {
    "Bash.ssh": { "background": { "permission": "deny" } },
    "Bash.pct": { "background": { "permission": "deny" } },
    "Bash.qm":  { "background": { "permission": "deny" } }
  }
}

An approval you grant once can become the standing rule, so the review the thread’s poster gave up on shrinks to the calls that are actually new.

What this is not

It is not a sandbox. It is a ceiling: the call is held on your machine, before the packet leaves, and a human decides. Spellings that need intent to produce remain open, and they are listed so you can weigh them: xargs -I{} ssh host {}, find -exec ssh host {}, su -c "ssh …", and an ssh ProxyCommand that runs a local command before connecting. Those are tracked, and they are the reason the restricted account and the sudoers file on the target stay in place. The two layers cover different things. The target’s floor bounds what a session can do once it is in. The ceiling in the harness bounds what gets sent, and writes down every call that did.

If you already run an agent against a Proxmox host, a jump box, or a fleet of containers, this is the same one-command install as everywhere else on this site. Try it in audit mode against your own setup first and read what the agent has been doing.

Share X HN LinkedIn
Get the next data drop
What agents actually cost, new tool-surface captures, and the occasional incident post-mortem — sent when we publish something worth your inbox, not on a schedule. Unsubscribe anytime.
Share: Twitter LinkedIn
More in Stop Your Agent From…
  1. 1. Stop your AI agent from running `rm -rf` on your filesystem — in three steps
  2. 2. Stop your AI agent from deleting your production database — in three steps
  3. 3. Stop your AI agent from touching files outside your project — in three steps
  4. 4. Stop your AI agent from rewriting your git history — in three steps
  5. 5. Stop your AI agent from leaking secrets in your `.env` file — in three steps
  6. 6. Stop your AI agent from leaking PII through tool calls — in three steps
  7. 7. Stop your AI agent from burning through your API budget — in three steps
  8. 8. Stop your AI agent from making payments without approval — in three steps
  9. 9. Stop your AI agent from dropping a Kubernetes namespace — in three steps
  10. 10. Stop your AI agent from escalating IAM permissions — in three steps
  11. 11. Stop your AI agent from being weaponized by a malicious package — in three steps
  12. 12. Stop your AI agent from wrecking a box over ssh — in three steps · you are here
Related posts

← back to blog