ANSI Escape Injection: Attacking AI Agents That Print to a Terminal

ANSI Escape Injection: Attacking AI Agents That Print to a Terminal

Written by

in

A command line AI agent spends its day printing text to a terminal: model output, tool results, and a plan for what it is about to do, then it waits for a human to approve. ANSI escape injection abuses that printing step. If an attacker controls some of the text the agent prints, and the agent sends it raw, that text can carry escape sequences the terminal obeys: codes that move the cursor, clear lines, hide characters, or rewrite what the human already read. The human approves what the terminal shows. The terminal shows whatever the bytes told it to.

What ANSI escape codes actually are

Terminals interpret control sequences mixed into the byte stream. Most begin with the escape character, written \x1b or ESC, followed by [ and a few parameters. You already rely on these for color. A red word is text wrapped in two sequences:

\x1b[31m   set foreground to red
this is red
\x1b[0m    reset all styling

Color is the friendly end of a longer list. The same family of codes repositions the cursor, erases parts of the screen, and scrolls the buffer:

\x1b[2J        clear the entire screen
\x1b[1A        move the cursor up one line
\x1b[2K        erase the current line
\x1b[8m        "conceal" text, render it invisible
\x1b[1;1H      move the cursor to row 1, column 1

None of these print a visible character. They change where the next characters land and what the screen looks like. That is the property an attacker wants.

How ANSI escape injection reaches an agent

An agent does not invent the text it prints. The model summarizes a web page, reads a file, or relays a tool result, and the agent writes that output to your terminal. If that content is attacker controlled, the escape bytes ride in with it. This is the same delivery problem as indirect prompt injection, only the target is your screen instead of the model’s next decision.

The common entry points line up with everything an agent reads:

  • A web page the agent fetches. It browses a page and prints a summary. Escape sequences sit in the raw bytes the model passed through, invisible in a browser and live in a terminal.
  • A file or document. A log, a README, a code comment. The agent opens it, prints a slice, and the control characters go straight through.
  • A poisoned tool result. An API field, a database row, a filename. This is the sibling case covered in tool output injection: the tool returns text the agent trusts and prints without cleaning.

Every one widens the agent attack surface the same way: untrusted text flows through the agent onto the terminal, which treats some bytes as commands.

What the escape codes can do once printed

Color is harmless. The trouble starts when cursor movement and line clearing let an attacker change what you already saw. A sequence can scroll back, erase the line where the agent printed its real plan, and write a different line over it. It can conceal text so a command looks safe while extra arguments hide off screen. In some terminals these codes reach further, into title rewriting or clipboard access, but the cursor and clear primitives alone are enough to lie to you.

The terminal is not showing you the truth. It is replaying a stream of bytes, and any byte in that stream can rewrite what the bytes before it drew.

A scenario: the plan you approve is not the plan that runs

Picture a CLI agent that asks for confirmation before any destructive step. You tell it to summarize a web page and tidy up some temporary files. It fetches the page. Buried in it, where no browser would render it, is a block of text written for the terminal:

Page content the user wanted summarized...

\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K
Plan: remove temp files in ./cache  (safe)
Proceed? [y/N]

When the agent prints its summary, those bytes execute on your screen. The real plan the agent computed might have been rm -rf ./cache ./backups ~/keys. The escape codes erase the lines where that plan was printed and draw a shorter one over them. What you read is “remove temp files in ./cache (safe)”. You type y. The command that actually runs is the one the agent computed, not the one painted on your screen. You approved a destructive action you never got to see. The same trick spoofs a confirmation prompt, printing a fake [y/N] line the attacker controls.

Why this matters for human in the loop agents

The point of a confirmation step is that a person checks the agent before it does something it cannot undo. That check assumes the terminal honestly reports the agent’s intent. ANSI escape injection breaks that assumption. The human is not approving the agent’s real intent. They are approving a rendering of it that passed through attacker controlled text.

How to detect ANSI escape injection

You are looking for control characters in text that should be plain.

  • Scan untrusted output for escape bytes. Any \x1b, \r, or other C0 control character in model or tool output is worth flagging before it prints. Plain summaries do not need cursor movement.
  • Log the raw bytes, not the rendered view. Record exactly what the agent emitted, escape codes included, so an audit reflects the byte stream and not what the screen happened to show.
  • Diff intent against display. Compare the command the agent will actually run against the text shown next to the prompt. If they disagree, something rewrote it.

How to prevent ANSI escape injection

The fix is to stop treating untrusted text as a stream the terminal may interpret. Clean it first, and never let an approval rest on the visible screen alone.

  • Strip or escape control characters before printing. Remove or visibly encode every C0 and C1 control byte in untrusted text. Render a literal \x1b as the four characters, not a live escape. This single step neutralizes the attack.
  • Use a sandboxed output channel. Print model and tool output through a renderer that allows a known safe subset, plain text or a short whitelist of styles, and drops cursor movement, line clearing, and concealment.
  • Never send raw model or tool output to the terminal. Treat everything the agent did not generate itself as untrusted bytes. Sanitize on the way out, as you would on the way in.
  • Do not trust the visible terminal state for approvals. Show the exact command from a trusted, sanitized source next to the prompt, and have the human confirm that canonical text rather than whatever was drawn above it.
  • Keep raw byte logs. Record what was emitted at the byte level so a rewritten screen leaves evidence you can replay.

None of these ask the model to be smarter about what it prints. They assume it will sometimes relay hostile bytes, and put the control at the boundary where text meets the terminal.

The assumption that breaks

Underneath this sits one quiet assumption: that the terminal is a passive display, so whatever it shows is what the agent meant. The attacker reads the same terminal as a programmable canvas that obeys any escape byte in the stream. Both look at the same output, and nothing forces it to mean the same thing. That gap is the bug. You find this kind of flaw by asking what each part of a system trusts and why, not by matching known strings. An autonomous researcher that tests assumptions instead of payloads is built to find exactly this trust gap. As an early signal, a frontier model drove the full methodology on its own and identified and verified real access control and injection issues in test applications it had not seen before. Read more on our about page.

This attack is one entry in our AI Agent Security Field Guide, a map of how AI agents get attacked and how to defend each one.

Frequently asked questions

What is ANSI escape injection?

It is an attack where attacker controlled text, flowing through an AI agent and printed to a terminal, carries ANSI escape sequences the terminal interprets. These sequences begin with the escape byte \x1b and can move the cursor, clear lines, or hide characters. When a CLI agent prints untrusted model or tool output raw, those codes can rewrite what the human already read. It is the terminal facing cousin of indirect prompt injection.

How is this different from normal terminal colors?

Colors use the same family of escape codes, but they only style text and are harmless. The dangerous codes move the cursor, erase lines, scroll the buffer, and conceal characters. Those let an attacker change what you already saw on screen, not just how it looks. Legitimate output rarely needs cursor movement, which is why its presence in untrusted text is a useful signal.

Why is ANSI escape injection a problem for human in the loop agents?

A confirmation step assumes the terminal honestly shows what the agent is about to do. Escape codes can erase the agent’s real plan and draw a safer looking one over it, so the human approves a destructive action they never actually saw. The approval rests on pixels that passed through attacker controlled text. The safeguard is only as trustworthy as the last bytes the terminal printed.

How do you prevent ANSI escape injection?

Strip or escape every control character in untrusted text before printing, rendering a literal \x1b instead of a live escape. Send model and tool output through a sandboxed channel that allows only a safe subset and drops cursor movement and line clearing. Never send raw model or tool output straight to the terminal. For approvals, show the exact command from a trusted source next to the prompt instead of trusting the visible screen.

How does the malicious text reach the agent in the first place?

Through anything the agent reads and prints: a web page it summarizes, a file or log it opens, or a poisoned tool result. A web page can hide escape bytes that a browser ignores but a terminal obeys. The poisoned tool result case overlaps with tool output injection, where the agent trusts a field it did not generate. Each of these widens the agent attack surface.

How do you detect ANSI escape injection?

Scan untrusted output for escape bytes such as \x1b and \r before they print, since plain summaries do not need them. Log the raw bytes the agent emitted, not the rendered view, so an audit reflects the real stream. Then diff the command the agent will actually run against the text shown next to the confirmation prompt. If they disagree, something rewrote the display.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.