SevyDevy

Guide · AI and GenAI

DuckDB Agent Mode Explained: How the CLI Detects and Adapts to AI Agents

20 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • DuckDB agent mode is how the DuckDB 2.0 command-line tool (CLI) prints results for an AI coding agent, a program that runs commands for you and reads what comes back.
  • It turns on by itself when three things hold. An agent sets an environment variable (a named setting) such as AI_AGENT, output goes into a pipe instead of a screen, and no format flag like -csv was given.
  • Results then come as compact Markdown tables, plain text with pipe characters between cells, showing each column's type. Big results show the first and last 20 rows, the total and a hash, a short fingerprint of all the rows.
  • Errors arrive as JSON, a fixed machine-readable format, and slow queries report their cost and progress. The goal is fewer tokens, the text pieces a model reads and pays for.
  • Beginners can stop after the section on trying it. Engineers then get DuckDB's own experiment (59% fewer tokens read, yet no cheaper runs), details read from the source, and a detection pattern to copy.

What goes wrong when an AI agent reads CLI output made for humans?

The agent pays for every character, including the ones that only exist to look nice. It can also misread what it sees, such as a cut-off result or an error written as prose.

Picture a common setup. You keep last month's orders in a DuckDB file and ask a coding agent which city sold the most. The setup is made up, but every output in this section was really measured on the DuckDB 1.5.6 CLI. DuckDB agent mode, new in DuckDB 2.0, exists to fix what you see below.

The agent runs the duckdb command and reads its output through a pipe, a connection that sends one program's output straight into another program. Nobody is looking at a screen. Yet the CLI still draws a neat box for human eyes.

  • Padding costs tokens. A seven-row summary by city came out as 786 bytes of box drawing. The same rows as a Markdown table took 333 bytes.
  • Big results are trimmed for a screen. For all 5,000 orders, the box showed 40 rows, three rows of dots and a footer: 5000 rows, 40 shown.
  • The workaround floods the context. When the agent asks for -markdown or -csv instead, the CLI prints every row. With -markdown, that was 5,002 lines and 135,054 bytes.
  • Errors are prose. A typo in a column name printed a sentence, a suggestion and a caret line on stderr, the separate channel programs use for errors.
  • Slow queries are silent. A 16-second count printed nothing at all until it finished, so the agent could not tell slow from stuck.

None of this is a bug. The output was designed for a person at a terminal. DuckDB 2.0's answer is to let the CLI work out who is reading.

Which words do you need before reading about DuckDB agent mode?

You need six terms. Each one comes up again in every section below.

  • CLI: a command-line interface, a program you run by typing a command. The DuckDB CLI is the duckdb command.
  • Stdout and stderr: the two text channels a program writes to. Results go to stdout, which means standard output. Errors and warnings go to stderr, standard error.
  • TTY or terminal: a screen where a person reads output. A program can ask whether its stdout is a terminal or a pipe.
  • Environment variable: a named setting a program inherits from whatever started it, such as AI_AGENT=1.
  • Token: a small piece of text, often part of a word, that a model reads. Model services charge per token.
  • Context window: the most tokens a model can hold in one request. Everything the agent has read so far competes for that space.

What is DuckDB agent mode in DuckDB 2.0?

DuckDB agent mode is a second output style in the DuckDB 2.0 CLI, built for AI coding agents instead of people. It changes how results, errors, query plans and progress are printed, not what the query computes.

DuckDB's announcement gives the reason in one line: coding agents increasingly run the DuckDB CLI on their users' behalf. A person skims a box and ignores the borders. An agent reads every byte, and every byte uses up its context window.

Think of a teacher who writes marks on the classroom board for students, then sends the same marks to the school office as a plain list. The board is the box view: big, spaced out and easy on the eye. The office list is agent mode: no decoration, every column labelled and a total at the bottom. Where the picture breaks: the teacher knows who will read each copy. The CLI has to guess from clues, and it can guess wrong.

The same jobs in the 1.5.6 CLI when piped and in 2.0 agent mode, all run by us (2.0 on a CLI built from the v2.0.0 source)
What the agent needs1.5.6 default output, piped2.0 agent mode
A small tableA box drawn with line characters and padding: 786 bytes for 7 rowsA Markdown table with no padding: 256 bytes for the same 7 rows. The post says 25 to 65% smaller than the box
Column typesA second header row, such as varchar and int64Inside the header, such as city:VARCHAR
A 5,000-row result40 rows, dot rows in the middle and a footer: 5000 rows, 40 shownFirst and last 20 rows, a line counting the omitted rows, the total and a hash: 1,098 bytes in all
A huge resultThe query runs to the end: 400 million rows took 2.8 seconds before 40 rows printedCounting stops about 100,000 rows past the display limit: a billion-row query stopped at 102,400 rows in 0.15 seconds
An errorPlain text with a caret line on stderrOne JSON object on stderr
A slow queryNothing on stderr: our 16-second query printed no progressA cost estimate first, then a progress line every 5 seconds on stderr
A query planA tree of boxes: 4,253 bytes over 84 lines for our queryOne step per line: 604 bytes over 8 lines for the same query

How does the DuckDB CLI know an AI agent is calling it?

It looks for three clues and needs all three. A known agent environment variable is set, stdout is not a terminal, and no output format flag such as -csv or -json was given.

The variable says an agent started this process. The pipe check means a person who types duckdb at a terminal still gets the box view, even if the variable happens to be set. The format check respects a deliberate choice: DuckDB's post says a format flag counts as one.

There is no single standard name for that variable yet. DuckDB's post says so plainly, so the CLI checks a list. The list includes the generic names AI_AGENT and AGENT, plus names that specific coding tools set.

How the DuckDB 2.0 CLI decides on agent mode

Notice that every automatic path to agent mode needs a pipe. DUCKDB_AGENT_MODE=1 or 0 also forces the mode when no flag does. In our test it beat -csv, which then set only the table format.

Diagram connections:

  • duckdb starts to -agent or -no-agent?
  • -agent or -no-agent? to Agent mode on (-agent)
  • -agent or -no-agent? to Normal output (-no-agent)
  • -agent or -no-agent? to Format flag given? (Neither)
  • Format flag given? to Normal output (Yes)
  • Format flag given? to Agent variable set? (No)
  • Agent variable set? to Normal output (No)
  • Agent variable set? to Is stdout a terminal? (Yes)
  • Is stdout a terminal? to Agent mode on (No: piped)
  • Is stdout a terminal? to Normal output (Yes: a person is watching)
  • duckdb starts: Run by a person or by an agent
  • -agent or -no-agent?: A command-line flag always wins
  • Format flag given?: -csv, -json, -markdown, -box and others
  • Agent variable set?: AI_AGENT, AGENT or a tool-specific name
  • Is stdout a terminal?: Screen means a person; pipe means a program
  • Agent mode on: Typed compact tables, caps, hash, JSON errors
  • Normal output: Box tables, or the format you asked for

You can also force the choice. The -agent flag turns the mode on and -no-agent turns it off. Setting DUCKDB_AGENT_MODE=1 or DUCKDB_AGENT_MODE=0 does the same from the environment. DuckDB's docs say the command-line flags take precedence over that variable.

To see what the CLI decided, run the .show command. It prints an agent line that reads off, on, or the name of the agent it detected. For AI_AGENT or AGENT, the name is the variable's value, so AI_AGENT=1 shows 1. The 1.5.6 CLI has no such line, and it rejects -agent as an unrecognised option.

What does DuckDB agent mode print instead?

It prints the same answers in a shape built for a reader that pays per character and cannot squint. There are five changes, and each one fixes a problem from the opening section.

Compact tables with types in the header

Results become Markdown tables, rows of values separated by pipe characters, with no spaces added for alignment. Each header cell carries the column type. For the city summary, the header row reads like this in DuckDB's format: city:VARCHAR, n:BIGINT, total:DOUBLE, each between pipes.

Those three types are what DuckDB 2.0.0 returned for our query in the Python package. The type matters to an agent. It tells it, for example, that total is a floating-point number, so 35607.09999999999 is rounding noise, not a data error.

Caps on big results, with an honest footer

Results up to 1,000 rows and 10,000 bytes print in full. Anything bigger shows the first 20 and last 20 rows, a single row saying how many rows were left out, and a footer. The footer gives the total, a hash, and the dot command that shows everything.

DuckDB's post gives this footer as an example: first 20 and last 20 of 5000 rows (.maxbytes 0 for all), hash 7389c2f5ff3cefb6. An agent cannot mistake that for the whole result. Long text cells are also cut at 500 characters, with a marker such as …(+4500 chars) saying how much was cut.

For a huge result, the CLI does not wait for the end. Once it has passed the cap, it keeps counting rows for a while and then stops the query. The footer then says first 20 of at least N rows, query stopped early.

What agent mode does with a big result

Follow the No arrows. A result either prints whole, prints as a sample with an exact total, or stops early with a lower bound.

Diagram connections:

  • Agent's SELECT to Within the caps?
  • Within the caps? to Print every row (Yes)
  • Within the caps? to Keep counting rows (No)
  • Keep counting rows to Result ended in time?
  • Result ended in time? to First 20 and last 20 (Yes)
  • Result ended in time? to Stop the query early (No)
  • Agent's SELECT: Rows stream into the CLI's table printer
  • Within the caps?: 1,000 rows and 10,000 bytes by default
  • Print every row: Footer with count and hash for 0 or 10+ rows
  • Keep counting rows: About 100,000 rows past the cap
  • Result ended in time?: Did the rows run out before that limit?
  • First 20 and last 20: Footer: exact total, hash, how to see all
  • Stop the query early: Footer: first 20 of at least N rows

A hash to compare results

A hash is a short code computed from data, here 16 hexadecimal digits. If two runs print the same hash, they almost certainly returned the same rows, though possibly in a different order. An agent that rewrites a query can compare hashes instead of rereading thousands of rows.

Errors as JSON on stderr

In agent mode, the CLI turns on DuckDB's errors_as_json setting and prints each error as one JSON object on stderr. Below is what our 2.0 CLI build printed for the same typo. It drops the engine's candidates field, because the message already lists the candidates.

json
{"exception_type":"Binder","exception_message":"Referenced column \"amout\" not found in FROM clause!\nCandidate bindings: \"amount\"","error_subtype":"COLUMN_NOT_FOUND","position":"7","location":"[7,5]","name":"amout"}

An agent can now read error_subtype and see COLUMN_NOT_FOUND, instead of guessing from English text. The name field gives the bad column and position gives the character where it starts.

Plans, cost estimates and progress

EXPLAIN, the command that shows how DuckDB plans to run a query, switches to a compact format with one step per line. Before a statement the planner expects to read about a million rows or more, the CLI prints that estimate to stderr. Long queries then print a progress line every 5 seconds, also on stderr.

Keeping these on stderr matters. A tool that captures stdout alone still gets a clean result, while the agent sees enough to decide whether to wait or cancel.

How do you try DuckDB agent mode and check it worked?

Install the 2.0 CLI, force the mode with -agent, and confirm with .show. You do not need an agent to test it.

  1. Install the DuckDB 2.0 CLI

    On Linux or macOS, duckdb.org's install page gives curl https://install.duckdb.org piped into bash, under its 2.0.0 tab. Docker users can run the duckdb/duckdb:2.0.0 image. Pip install duckdb gives the Python package, not the CLI.

  2. Check the version

    Run duckdb --version and expect a line starting v2.0.0. An older CLI will reject the -agent flag as an unknown option.

  3. Force agent mode on

    Run duckdb -agent -c "SELECT 42 AS answer;", the example from DuckDB's docs. Expect a Markdown table whose header names the column and its type.

  4. Ask the CLI what it decided

    Run duckdb -agent -c ".show" and look for the agent line. Off means normal output. Anything else, such as on or an agent's name, means agent mode.

  5. Test automatic detection

    Run AI_AGENT=1 duckdb -c ".show" piped into cat, so stdout is a pipe. The agent line should read 1, the value of AI_AGENT. Without the pipe, expect off.

  6. Measure the difference

    Run the same query with -agent and with -no-agent, each piped into wc -c. That prints the size in bytes, which is a rough stand-in for tokens.

We ran these steps on our build from the v2.0.0 source, not on the official binary, as the October 2026 note explains. If the startup line announcing agent mode bothers you, the docs say to put .startup_text none as the first line of the ~/.duckdbrc file.

Does DuckDB agent mode save tokens and money?

It saved tokens in DuckDB's own experiment, but not money or time. That gap is the most useful lesson in the whole release.

The DuckDB team ran one coding agent, with one model, on the 22 TPC-H questions, a standard set of business queries, over a test dataset of about 29 GB. Each question ran three times with agent mode and three times without, 132 runs in all. Every run got the right answer.

Pause and predict: if the database output an agent reads shrinks by more than half, how much cheaper should each run get? Hold your guess and check it against the table.

DuckDB's experiment report, 66 runs per arm, on a development build of 2.0 (their numbers, not ours)
MeasureWithout agent modeWith agent mode
DuckDB output read back by the model123.6k tokens50.8k tokens (59% fewer)
Same output in bytes312 KB162 KB (48% fewer)
Total cost of the runs13.68 US dollars14.18 US dollars (4% more)
Agent turns224237 (6% more)
Correct answers66 of 6666 of 66

Why did 59% fewer tokens not make anything cheaper? The report's author points at the agent's own instructions, which run to tens of thousands of tokens and are re-read every turn. Next to that, DuckDB's output was a small slice of the bill.

A follow-up asked the agent to put full results in its answer. There the saving shrank to 19 to 31%. The report notes that the 10,000-byte cap sometimes forced an extra run to fetch rows that had been cut.

One more finding is easy to miss. In the follow-up, the agent sometimes added -csv or -markdown by itself, and a format flag switches detected agent mode off. In one condition, that happened in 7 of 26 agent-mode runs. So part of the time, the mode under test was not even on.

Which DuckDB agent mode details are easy to miss as of October 2026?

Five details matter if a script reads this output. The first three are in DuckDB's post, and only the order rule is also on the docs page. The last two appear only in the v2.0.0 source, so treat them as undocumented as of 28 October 2026 and pin your version. We confirmed all five on our 2.0 build.

  • The footer is skipped for results of 1 to 9 rows. The post says it appears only for empty results, results of 10 rows or more, and capped output.
  • The hash ignores row order. The same rows in a different order give the same hash, so it cannot prove that an ORDER BY change worked.
  • Early stop has a fixed look-ahead. The post says the query stops after 100,000 more rows, and the count becomes a lower bound with no hash.
  • Column order does count. In the source, each row's hash mixes its cells in sequence, so swapping two columns changes it.
  • The hash covers full values. The source computes it before long cells are cut, so two results that differ only after character 500 get different hashes.

One combination is missing from the docs. If DUCKDB_AGENT_MODE=1 is set and the caller also passes -csv, agent mode stays on. In our test, rows came out as CSV, but errors were still JSON and the startup line still printed. The same happened with -agent plus -csv. Treat this as undocumented as of 28 October 2026.

How can your own CLI detect an AI agent and adapt its output?

Copy the shape of DuckDB's rules: an explicit flag wins, a caller's own format choice comes next, and the guess from the environment and the pipe comes last.

Here is a small Python tool that prints the city counts from our example. It is our own sample, not DuckDB code. It uses a made-up REPORT_AGENT_MODE variable in the role DuckDB gives DUCKDB_AGENT_MODE. DuckDB lets DUCKDB_AGENT_MODE beat a format flag. Our sample lets --csv win instead, which keeps a small tool simple.

python
import os
import sys

AGENT_VARS = ["AI_AGENT", "AGENT"]  # DuckDB checks these and some tool-specific names


def pick_format(args):
    if "--agent" in args:
        return "agent"                   # an explicit flag always wins
    if "--no-agent" in args:
        return "human"
    if "--csv" in args:
        return "csv"                     # the caller chose a format on purpose
    forced = os.environ.get("REPORT_AGENT_MODE")  # like DUCKDB_AGENT_MODE
    if forced in ("0", "1"):
        return "agent" if forced == "1" else "human"
    agent_var_set = any(os.environ.get(name) for name in AGENT_VARS)
    if agent_var_set and not sys.stdout.isatty():
        return "agent"
    return "human"


rows = [("city_0", 715), ("city_1", 715), ("city_2", 714)]
fmt = pick_format(sys.argv[1:])
if fmt == "agent":
    print("| city:VARCHAR | n:BIGINT |")
    print("|---|---|")
    for city, n in rows:
        print(f"| {city} | {n} |")
elif fmt == "csv":
    print("city,n")
    for city, n in rows:
        print(f"{city},{n}")
else:
    print(f"{'City':<10}{'Orders':>8}")
    for city, n in rows:
        print(f"{city:<10}{n:>8}")

Save it as report.py and run it four ways. With AI_AGENT=1 and output piped into cat, it prints the typed Markdown table. In a terminal with the same variable, it prints the padded human view. With --csv it prints plain CSV, and with no agent variable it stays human.

What just happened? The line with sys.stdout.isatty() asks the operating system whether a screen is attached. That one call is what keeps a person in an agent's terminal from getting agent output. We ran eight cases of flags, variables and pipes on Python 3.13.

A few rules travel well beyond this sample. Report the decision somewhere, as DuckDB's .show does. Keep results on stdout and progress or errors on stderr. Say when output is cut, and say how to get the rest.

What are the limits of DuckDB agent mode?

Detection is a guess from a list, and a guess can miss. An agent that sets none of the listed variables gets the human view. DuckDB's post asks people to report agents that are not detected.

The caps can cost an extra round trip. When an agent truly needs row 500 of 5,000, it must rerun with .maxrows -1 and .maxbytes 0, or narrow the query with a WHERE clause.

Format flags quietly turn it off. That is correct when a caller wants CSV for a file. But in DuckDB's experiment, the agent picked those flags out of habit.

Agent mode also makes no query cheaper to run. For that, DuckDB's docs suggest SET max_execution_time, a limit in milliseconds, and DESCRIBE, which returns a query's columns and types without running it.

How do you explain DuckDB agent mode in a system design interview?

Frame it as designing one tool for two kinds of caller: people and programs that pay per token.

  1. Name the two readers

    A person skims and wants alignment. An agent reads every character, pays for it, and needs types, totals and clear error codes.

  2. Explain detection with an escape hatch

    Guess from an environment variable plus a pipe check, and let an explicit flag or format choice always win. Say why the pipe check protects people.

  3. Bound the output

    Cap rows, bytes and cell length, keep the first and last rows, and state the total. Mention a lower bound when the query is stopped early.

  4. Make results comparable

    Offer a hash so the caller can compare runs cheaply. Point out that an order-independent hash cannot check sorting.

  5. Separate the channels

    Results on stdout, structured errors and progress on stderr. That keeps parsing simple for the calling tool.

  6. Measure the right thing

    Quote DuckDB's result: 59% fewer output tokens, yet about the same cost. Then say which metric your team would track, such as turns, context used or wrong answers.

Questions people ask

Do I need to change anything to use DuckDB agent mode?
Usually not. If your coding agent sets one of the variables DuckDB checks and reads the CLI through a pipe, the 2.0 CLI switches on its own. If it does not, add -agent to the command or set DUCKDB_AGENT_MODE=1. Run .show to confirm what the CLI decided.
How do I turn DuckDB agent mode off?
Pass -no-agent on the command line or set DUCKDB_AGENT_MODE=0. Passing an output format flag such as -csv or -json also switches detected agent mode off, because DuckDB treats a format flag as a deliberate choice. The docs say command-line flags take precedence over the environment variable.
Does agent mode change query results?
No. It changes how results are printed, not what the query computes. Big results are shortened on screen to the first and last 20 rows, with the true total and a hash in the footer. After an early stop you get a lower bound instead. Use .maxrows -1 and .maxbytes 0 to print everything.
Is DuckDB agent mode available in the Python package?
No, agent mode belongs to the duckdb command-line tool, and the 2.0.0 Python package source has no agent-mode code. Some engine pieces it uses do exist there: we ran SET errors_as_json = true and EXPLAIN (FORMAT compact) on the duckdb 2.0.0 Python package and both worked.
Why does the hash not change when I add ORDER BY?
Because the hash ignores row order, as DuckDB's post says. In the v2.0.0 source, each row gets its own hash and the row hashes are added together, so any order gives the same sum. That makes the hash good for checking which rows came back, but useless for checking their order.
Will agent mode make my agent cheaper or faster?
Not by itself, going by DuckDB's experiment. Output tokens read back fell 59%, but total cost rose about 4% and turns rose about 6%. The report's author says the agent's own instructions dominated input tokens. Your results will depend on your agent and on how much database output it reads.

Sources and further reading

Practise this with Lala

Practise