AI Agent Security: Controls for a Self-Hosted Agent
AI agent security is the set of controls that decide what an agent that reads outside content and calls tools can do, who can steer it, and how you stop it. For a self-hosted agent the useful question is not “is it protected” but “which check stops which failure, and where does that check run”.
AI agent security covers four things: who may give the agent instructions, which tools a given turn may call, which actions wait for a human, and what bounds a run that nobody is watching. This page explains each one using POLYROB, an open-source (MIT), self-hosted AI agent, and points to the file in the code that implements it. It also states where those checks stop being enough, which is the part most overviews leave out.
What are the main security risks of an AI agent?
The OWASP AI Agent Security Cheat Sheet (read 2026-11-11) lists the risks that an agent adds on top of a plain language model. The ones that matter most for a personal agent are:
- Prompt injection, direct and indirect: instructions hidden in a web page, document or email that the agent reads.
- Tool abuse and privilege escalation: an agent using a tool that is more permissive than the task needs.
- Data exfiltration: sensitive information leaving through a tool call or an output.
- Memory poisoning: hostile text saved in memory so it influences later sessions.
- Excessive autonomy: high-impact actions taken with no human in the loop.
- Denial of wallet: unbounded agent loops that run up model costs.
- Supply chain attacks: a compromised third-party tool or data source.
Each of these has a corresponding control in the agent’s design. The sections below go through the controls in the order a message travels: who is allowed in, what the turn may do, what waits for you, what bounds the run, and what you can see afterwards.
Who is allowed to give an AI agent instructions?
The first control is identity. An agent that lives in chat apps receives messages from many people, and most of them must never be able to steer it.
POLYROB resolves exactly one access tier for every inbound message: owner, correspondent, group member or denied. With the access model on, the check fails closed (source: docs/guide/[email protected], section 1; core/surfaces/[email protected]). The tiers mean:
- Owner. The bound owner, or the person at the local terminal. Only this tier can steer the agent and approve things.
- Correspondent. A third party the agent itself wrote to first. Their reply reaches the session as framed data, never as a command.
- Group member. A person in a room you allowed. They can start a turn, but it runs a public, read-only session.
- Denied. Anyone else. No routing at all.
The tier is only as good as the proof of who sent the message, so each chat app proves it before the dispatcher sees the text. On every network chat app except Telegram, which checks its own allowlist first, a direct message from someone who is neither the owner nor a paired sender is denied. An email sender reaches the owner or correspondent tier only when the receiving server’s topmost Authentication-Results header shows dmarc=pass or an aligned DKIM or SPF pass, so a forged From line gets no tier. A WhatsApp webhook without WHATSAPP_WEBHOOK_SECRET rejects every payload, and a Signal setup refuses a plain-HTTP signal-cli daemon on a non-loopback host (source: docs/guide/[email protected], section 1).
A chat app starts locked. On Telegram, with no allowlist set, your first private message gets your numeric id and the exact polyrob config set ALLOWED_TELEGRAM_USER_IDS line, and the agent does not run on that message. With an allowlist set, a private message from any other id gets no answer at all (source: docs/guide/[email protected], “First contact on a chat surface”).
What does pairing change?
Pairing is off by default (POLYROB_REQUIRE_PAIRING, source: docs/[email protected]). Turn it on and an unpaired person in a private chat gets a one-time code that you approve with /pair approve <surface> <code>. A code works once and expires after 15 minutes. A paired sender can command the agent on that chat app with their own memory and work, and never reaches your approvals, owner commands or memory. Approve only a code you expected. Turning pairing off removes access for every paired sender on every chat app (source: docs/guide/[email protected]).
What can one turn of the agent do?
Identity answers who is talking. The next control answers what the turn may touch, whatever the text says.
Why does the agent classify every tool?
core/tool_capabilities.py is a single table that marks each tool as money, high-impact, exec or blocked for delegated sub-agents. register_optional_tool refuses to register a tool that is not classified there, which closes the failure where a new tool silently skips every gate (source: docs/guide/[email protected], section 2; core/[email protected]). A delegated sub-agent never receives a blocked toolset.
How does the agent treat untrusted content?
Output from tools that read outside text is framed before the model sees it, in an untrusted_tool_result block, and the framing is on by default (UNTRUSTED_TOOL_RESULT_WRAP, source: docs/[email protected]). That is prompt-level framing, so a model can still ignore it. The structural backstop is the correspondent taint gate: while the latest input on a session is third-party text, a pre-tool hook denies the high-impact tools, which include outbound messaging, code execution, delegation and the browser (source: agents/task/agent/core/[email protected]). The page on prompt injection examples walks through seven concrete payloads and which check answers each. The browser is the tool that reads the most outside text, and its navigation limits are covered in the AI agent browser guide.
What can a group chat member make the agent do?
A session bound to a group chat is public. It carries none of the owner’s private state, and it runs a fixed read-only toolset, task,web_fetch,defi_data by default, which the environment can only narrow. A pre-tool hook denies scheduling, skill changes, cross-session recall and delegation by name, and allows outbound messages only to that room (source: docs/guide/[email protected], “The room rail”; core/surfaces/[email protected]).
How is the shell guarded?
Three tools reach the host on purpose: shell, process and self_env. Each asks the compute posture first. AGENT_COMPUTE_POSTURE defaults to 0, which means confined: no persistent shell. Posture 1 adds a persistent shell inside a container, posture 2 adds approval-gated self-maintenance verbs, and posture 3 runs the shell on the host, only for the owner’s own turn typed at a foreground terminal on a box marked POLYROB_LOCAL (source: docs/[email protected]).
On top of the posture, the command guard in core/security/command_guard.py classes every shell command before it runs. A floor of commands is refused everywhere and no approval lifts it: deleting a root or home tree, a fork bomb, writing a raw disk, a root curl | sh, reading the wallet seed, stopping the agent’s own service. A second class, which includes recursive delete, force-push, sudo and pipe-to-shell, waits for the owner wherever the shell tool is approval-gated. In an unattended run nobody can answer, so the owner’s unattended_dangerous_shell setting decides: sandbox, the default, runs it inside the sandbox and never on the host, and refuse refuses it (source: docs/guide/[email protected], section 2).
Which actions wait for a human?
OWASP’s guidance for human-in-the-loop controls is to require explicit approval for high-impact or irreversible actions, set autonomy boundaries by risk, and keep an audit trail (OWASP cheat sheet, section 4, read 2026-11-11). POLYROB’s version of this is the owner queue.
- Approval hooks are wired to fail closed. A denial, a timeout or a crashing approval provider blocks the action instead of allowing it (source:
tools/controller/[email protected]). - The queue is durable. It survives a restart and can be answered from your phone with
/approveand/reject, or with the one-tap token printed next to each item in/pending./inboxlists everything waiting on you, blocking items first (source:docs/guide/[email protected], “Approving what the agent proposes”). - Only slash verbs and tap tokens decide an item. A plain “approve” typed in chat is a message to the agent, not a decision.
- A late approval sends exactly what was approved. When you approve a queued post or mail after its run has ended, the agent sends those parameters once and does not re-run the job that asked.
- An answer to an open question must come in words you typed in that message. Text from a page, a mail or a search result cannot decide an ask.
Approval is a checkpoint, not a sandbox. The documentation says so: it does not judge whether the approved action is itself safe, and it covers only the tools that are on the gated list. In autonomy mode most approvals become allow, audit and notify, while self-modification and dangerous shell commands stay on the owner queue.
What bounds a run when nobody is watching?
This is the control that answers “denial of wallet” and runaway loops, and it is the one a personal agent needs most, because the point of a personal agent is that it works while you are away.
- A step cap.
AUTONOMOUS_MAX_STEPSdefaults to 150 for an autonomous run: a goal, a cron job, a delegation or a worker. No tool argument, goal payload or worker profile can raise it. The operator owns it (source:docs/[email protected]). - A spend cap.
AUTONOMOUS_RUN_BUDGET_USDdefaults to 2.0. The run halts before the next step once the spend since that run started reaches the cap, and sub-agent spend counts against it. It is a per-run cap, not a daily or monthly budget, and the check fails open on a probe error, so treat it as a guard and not a guarantee (source:docs/[email protected]). - A time limit. A cron job that sets no maximum duration gets 600 seconds per run by default (
CRON_DEFAULT_MAX_DURATION_SEC, ceiling 1800). - Autonomy is a dial you set.
AUTONOMY_MODEdefaults tosupervised.autonomousis only effective on a single-owner deployment with a bound owner, and otherwise clamps back tosupervisedwith a warning (source:docs/[email protected]). - A delivery bound. Messages the agent sends you ride one rail with a daily cap of 30 and an hourly limit of 10, so a runaway loop cannot fill your chat. Anything held back is kept and shown by
/missed(source:docs/guide/[email protected], “What did I miss?”).
How do you stop an agent immediately?
Send /pause (alias /halt). It is a slash command on purpose: it runs with no model call, no queue and no tool in the way. Goal dispatch, cron ticks, self-wake re-entries and correspondent replies stop within seconds, and in-flight work is cancelled. Your own chat with the agent stays on, along with crash, security and credit alerts. You can scope a pause, for example /pause background or /pause for 2d, and /resume lifts it (source: docs/guide/[email protected]).
The state lives in one file, AUTONOMY_PAUSE.json, read by every process. An unreadable record is treated as a pause of everything, and a restart does not clear it. After a pause, a pause_violation health item names any autonomous activity recorded since, and you should treat it as an incident.
How do memory and skills stay under review?
Memory poisoning and stored injection are the cases where text is planted now and acted on later. Two controls apply.
Skill installs are scanned and quarantined. Installing a skill runs a threat scan and then holds it in quarantine until you run polyrob skills approve <id>. A scan hit or a scanner error refuses the install. Only a local folder you own installed with --trust local, or an official or trusted tap, goes active at once (source: docs/guide/[email protected]; see agent skills for the full lifecycle).
Skills the agent writes are different, and the gap is documented. A skill that your own turn or job writes is active at once with a rollback, also when that run read a web page or an email. Text planted on that page can then reach later runs through the skill, and the threat scan and the rollback are the only checks. A chat that holds someone else’s message, a group or another user’s turn still lands only a draft. The project’s advice is to check a job’s skills with polyrob skills info <id> before you let it read untrusted pages (source: docs/guide/[email protected], section 3, b2).
The memory write scan, MEMORY_THREAT_SCAN, is off by default (source: docs/[email protected]). If you let an agent ingest pages you do not trust, that is a setting to know about.
What can you see after the fact?
OWASP asks for audit trails. POLYROB writes a typed row for each decision to telemetry_events.db, with no message text, at most a length and an 8-character hash. core/security_digest.py rolls these up into the security section of the status view and the daily brief. The categories are inbound routing decisions (allowed and refused), gate refusals, threat flags, limiter trips and model cost per chat (source: docs/guide/[email protected], section 6).
Two commands are worth knowing. /why [n] lists the last refusals with the tool and the gate’s reason word for word, read from the recorded events and not from the agent’s own summary. polyrob doctor leads with the pause line and flags a configuration that has drifted.
A deep scan is up to you. The package does not ship one. The repository provides requirements.lock for pip-audit and .gitleaks.toml for gitleaks, and a scan that writes its verdict to <data>/ops/security_scan.json shows on the status line. A check that could not run is reported as not run, never as a pass.
Where do these controls stop working?
The project’s own security page says it first: almost every gate is a policy check running in the same process as the agent, and none of them is a hard security boundary. The hard boundary is the operating system or container (source: docs/guide/[email protected], introduction). So the documented gaps are these:
| Area | What the controls do | What they do not do |
|---|---|---|
| Process identity | Nothing drops privilege for you | A root process puts the host behind in-process checks |
| MCP servers | Child environment is a fixed allowlist, credentials excluded | A server is a full sibling process with the agent’s privileges |
| Command guard | Refuses known destructive shapes | Stop a determined author of a new shape |
| Approval | Blocks an ungated action until a decision | Judge whether an approved action is harmful |
| Tenant isolation | user_id-scoped checks in one process | Separate tenants at the OS level |
| Browser | Runs as its own principal over CDP | Remove the shared kernel |
Tenant isolation deserves a plain statement. POLYROB’s multi-tenant model is a single process with many sessions, and tenancy is software checks. If your threat model includes one tenant attacking another, run separate instances with separate data directories instead of relying on a flag inside one server.
How do you harden a self-hosted AI agent?
These steps come from the deployment advice in the project’s documentation (source: docs/guide/[email protected], sections 4 and 5).
- Run as a non-root user. Use a systemd unit with
User=,NoNewPrivileges=true,ProtectSystem=strict,ProtectHome=trueandPrivateTmp=true. The docs call this the single highest-leverage change. - Leave
AGENT_COMPUTE_POSTUREat 0 unless you are deliberately building software with the agent. - Use
CODE_EXEC_BACKEND=docker, never the local subprocess backend, for any deployment that is not one fully trusted operator on their own box. For a server, the sandbox guard already refuses non-sandboxed code execution. The AI agent sandbox guide lists the container flags and the backends. - Keep the MCP server list short and treat each binary as trusted code. Adding a server from chat accepts HTTPS URLs only, and a local command is refused. The MCP client guide explains how servers are declared.
- Lock the chat apps. Set the owner id for each app, leave pairing off unless you need it, and approve only codes you expected.
- Review skills before a job reads pages you do not trust.
- Know your stop switch and test it once: send
/pause, check the reply, then/resume.
For the install steps behind these, see how to self-host an AI agent. For how goals, schedules and caps work together, see the guide to goals, schedules and unattended runs.
Frequently asked questions
How risky is it to run an AI agent on my own machine?
The risk follows the privileges you give it and the boundary you put under it. A local agent runs as your user and can reach what your user can reach. Keep the compute posture at 0, use a container for code execution, run as a non-root user on a server, and keep approvals on for high-impact actions.
What is the biggest security risk with an AI agent?
For an agent that reads outside content, indirect prompt injection is the most common entry point, and excessive autonomy decides how much damage it can do. The controls that matter most are the ones that limit tools for a tainted turn, queue high-impact actions, and cap each unattended run.
Can an AI agent be made immune to attacks?
No. A language model reads instructions and data in the same channel, and the checks around it are mostly software running beside it. Layered controls reduce the risk and make failures visible. They do not remove it, and any page that says otherwise is overselling.
How do I stop an AI agent that is misbehaving?
With POLYROB, send /pause in any chat app, in the terminal or in the console. It needs no model call and survives a restart. From the box, run polyrob autonomy pause. To keep the loops from starting at all, run polyrob autonomy off.
Does an AI agent need its own permissions model?
Yes. Treat the agent like a new user with a narrow role: decide who can instruct it, which tools it can call for each kind of input, which actions wait for you, and what bounds an unattended run. Those four answers are most of agent security.
AI agent security and the next step
Agent security is a list of specific checks and a plain statement of where they end. To run POLYROB with these defaults, start with the install page and read the docs. The full security model is public: security-model.md at v1.7.7, along with the owner controls guide.
POLYROB is an open-source AI agent you run and own. Install it or read the docs.