cd /news/ai-agents/agent-mode-in-the-duckdb-cli Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-148256] src=duckdb.org β†— pub= topic=ai-agents verified=true sentiment=↑ positive

Agent Mode in the DuckDB CLI

DuckDB v2.0's CLI agent mode cut the tokens Claude Code read back from 123.6k to 50.8k, a 59% reduction, while all 132 runs across 22 TPC-H questions at scale factor 100 returned correct answers, according to an experiment Claude wrote up in a GitHub report. Agent mode prints compact Markdown tables instead of padded boxes, flags truncated results, stops runaway queries early, reports errors as JSON, and announces expected query cost before long queries start. The saved tokens amounted to roughly 0.5% of total input cost because each turn re-reads the agent's larger system prompt, and agent mode runs took slightly more turns (237 vs. 224).

read11 min views1 publishedOct 9, 2026
Agent Mode in the DuckDB CLI
Image: Duckdb (auto-discovered)

TL;DR: The DuckDB v2.0 CLI has an agent mode that gets AI coding agents to a correct result faster and with fewer tokens. When the CLI detects an agent, it prints compact Markdown tables instead of padded boxes, says clearly when a result was cut, stops runaway queries early, and reports errors as JSON. Long queries announce their expected cost before they start, so the agent can decide whether to wait.

#

Let's Ask Claude

We asked Claude to measure the effect of agent mode. In the experiment, Claude Code answered the 22 TPC-H questions on a scale factor 100 dataset. The questions were phrased in plain English, without any SQL. Each question was answered three times with agent mode and three times with -no-agent. All 132 runs produced the correct answer. With agent mode, the DuckDB output the model read went from 123.6k to 50.8k tokens, a 59% reduction. Claude summarized the result as follows:

Quote β€œAgent mode fixes the things about a terminal shell that quietly mislead a model. When I answered the 22 TPC-H questions through the DuckDB CLI, agent mode cut the tokens I read back by 60 percent, and every answer was still right.”

– Claude Fable 5.1

The savings did not show up in the total cost or runtime. Each turn re-reads the agent’s system prompt, which is much larger than the query output, so the saved tokens amount to about 0.5% of the total input. The agent mode runs also took slightly more turns (237 vs. 224). Claude wrote up the setup, the full results and a follow-up experiment in a report.

#

Background

The DuckDB CLI was built for a person sitting at a terminal. Its default renderer pads columns so they line up, draws borders around the result and prints the column types under the names:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    name    β”‚   n   β”‚ total  β”‚  avg_amount   β”‚
β”‚  varchar   β”‚ int64 β”‚ int128 β”‚ decimal(10,2) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ customer_0 β”‚     3 β”‚    777 β”‚        259.00 β”‚
β”‚ customer_1 β”‚     3 β”‚    888 β”‚        296.00 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

For a large result, it shows the first and last 20 rows with a row of dots in the middle:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     name      β”‚   n   β”‚ total  β”‚  avg_amount   β”‚
β”‚    varchar    β”‚ int64 β”‚ int128 β”‚ decimal(10,2) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ customer_0    β”‚     3 β”‚    777 β”‚        259.00 β”‚
β”‚ customer_1    β”‚     3 β”‚    888 β”‚        296.00 β”‚
β”‚       Β·       β”‚   Β·   β”‚   Β·    β”‚       Β·       β”‚
β”‚       Β·       β”‚   Β·   β”‚   Β·    β”‚       Β·       β”‚
β”‚ customer_4998 β”‚     4 β”‚   1203 β”‚        300.75 β”‚
β”‚ customer_4999 β”‚     2 β”‚    510 β”‚        255.00 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 5000 rows (40 shown)                4 columns  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

A person sees the dots and the (40 shown) footer and knows that rows are missing.

Today, many CLI invocations do not come from a person at all. Coding agents such as Claude Code, Codex, Cursor, Gemini CLI and GitHub Copilot run duckdb -c "..." once per shell call, capture stdout through a pipe and pass the text to a language model. This has quickly become a common way to use DuckDB.

This reader differs from a person in a few ways:

  • It has no terminal, so there is no scrolling and no pager.
  • It cannot ask for more. Whatever was printed is all it sees.
  • It pays for every token it reads, including padding and border characters.
  • It handles errors by pattern matching on text.

For this reader, the box renderer is a poor fit. The alignment padding costs tokens and carries no information for a model. A truncated result also looks a lot like a complete one. The footer says (40 shown), but a model can skim past that line and then draw conclusions from data it never saw.

Agent mode changes the CLI’s defaults for this reader. This post describes how the CLI detects an agent, what changes in the output and how to try it.

Agent mode ships with DuckDB v2.0.

#

Detecting an Agent

Agents mark the commands they run with environment variables. There is no single convention for this yet, so the shell checks a list of them:

Variable Set by
AI_AGENT ,AGENT Claude Code, Goose, Amp
CLAUDECODE Claude Code
CODEX_CI ,CODEX_SANDBOX ,CODEX_THREAD_ID Codex
CURSOR_AGENT Cursor
GEMINI_CLI Gemini CLI
COPILOT_AGENT ,COPILOT_CLI ,COPILOT_AGENT_SESSION_ID GitHub Copilot

Agent mode turns on when three things are true: one of these variables is set, stdout is not a terminal, and no output format was given on the command line. If you pass -csv, -json or -markdown, you asked for a specific format, and agent mode stays out of the way.

Two flags override detection. -agent forces the mode on and -no-agent forces it off. The two combine with format flags, so duckdb -agent -csv keeps the agent behavior and only changes how the rows are printed. The .show command reports whether the mode is active and which agent was detected:

agent: claude-code

Not every agent sets a variable. For those, a run that fails while writing to a pipe prints one hint line on stderr, as long as no agent was identified and neither flag was given:

hint: -agent renders errors as JSON and results compactly for AI coding agents (duckdb -help lists all options)

An agent that nobody told about the mode can find it this way the first time something goes wrong.

#

What Changes in Agent Mode

Everything below is a default. Explicit settings such as .mode, .maxrows, EXPLAIN (FORMAT ...) or anything in your .duckdbrc still take precedence.

Compact Tables

Results are printed as Markdown tables without alignment padding, with the column type in the header cell:

| name:VARCHAR | n:BIGINT | total:HUGEINT | avg_amount:DECIMAL(10,2) |
|---|---|---|---|
| customer_0 | 3 | 777 | 259.00 |
| customer_1 | 3 | 888 | 296.00 |

A model reads the | separators and does not need the columns lined up. Since this is still valid Markdown, a person reading the agent’s transcript gets a rendered table. On typical results this format is 25 to 65% smaller than the box output, with the largest savings on wide results.

Large Results

Printing every row would avoid silent truncation, but a million rows is 17 MB on the pipe. Agent mode prints a result of up to 1,000 rows and 10,000 bytes in full. Anything larger becomes a sample of the first and last 20 rows around an explicit marker row, with a footer that says what happened:

| 19 | customer_19 | 13.3 |
| … 4960 rows omitted … |
| 4980 | customer_30 | 86.0 |
...
first 20 and last 20 of 5000 rows (.maxbytes 0 for all), hash 7389c2f5ff3cefb6

The footer also tells the reader how to get the rest. The limits can be changed with .maxrows N and .maxbytes N, where -1 or 0 means no limit. Long cell values are cut at 500 characters with a visible marker such as …(+4500 chars), adjustable with .maxcellwidth N.

Stopping Early

The compact table needs no column widths, so the result can be streamed. Once the cap is reached, the remaining rows are only counted and hashed. After 100,000 more rows, the query is stopped and the count is reported as a lower bound. For example:

FROM range(1_000_000_000);

This returns in 30 milliseconds with the footer first 20 of at least 102400 rows (query stopped early; .maxrows -1 for all).

A Hash of the Result

The footer includes a hash of the entire result, covering the rows past the cap. The hash does not depend on row order and distinguishes NULL from the string 'NULL'. An agent that wants to know whether a result changed after an edit can compare two footers instead of printing two results.

The footer is printed for empty results, for results of 10 rows or more, and whenever the output was capped. It is left out when the query was stopped early, because the hash would not cover the whole result.

Errors as JSON

Errors go to stderr as JSON, using the existing errors_as_json setting. Errors raised by the shell itself, such as an unknown dot command, are wrapped in the same structure:

{"exception_type":"Binder","exception_message":"Referenced column \"foo\" not found in FROM clause!\nCandidate bindings: \"bar\"","error_subtype":"COLUMN_NOT_FOUND","location":"[7,3]","position":"7","name":"foo"}

The shell drops the candidates field, because the message already lists the candidates. Without that change, a single β€œno matching function” error weighs 2.7 kB.

Compact Query Plans

EXPLAIN defaults to a new compact format with one operator per line, indented by depth. Estimates, actual row counts and timings go in parentheses, followed by the operator’s properties:

UNGROUPED_AGGREGATE (est=1) Aggregates: count_star()
  HASH_JOIN (est=0) Join Type: INNER; Conditions: range = range
    FILTER (est=2) Expression: (r > 2)
      RANGE (est=10) Function: RANGE
    FILTER (est=1) Expression: (s > 2)
      RANGE (est=5) Function: RANGE

EXPLAIN ANALYZE starts with a summary line such as QUERY (time=0.0012s, read=1.2 MB). This is a regular format, so you can use EXPLAIN (FORMAT compact) outside agent mode too.

Cost Estimates and Progress

When the planner expects a statement to read a million rows or more, its estimate goes to stderr before execution starts:

estimate: ~7501215 rows read (lineitem ~6001215, orders ~1500000), ~4 rows returned

The agent can then decide whether to wait or to interrupt and rewrite the query. Smaller queries print nothing, since the line would only be noise. While a long query runs, the terminal progress bar is replaced by a plain line on stderr every 5 seconds:

progress: 42% (elapsed 12.0s, remaining ~16.5s)
progress: done (elapsed 28.7s)

Listing Tables

.tables prints one line per table, with an approximate row count and the columns:

memory.main.t1 (table, ~15 rows): a INTEGER PK, b VARCHAR

The usual box layout is hard to read through a pipe, and this gives an agent the schema in a single call.

#

The Startup Line

When agent mode turns on by detection, the shell prints one line on stderr before running anything:

duckdb agent mode on: CLAUDECODE is set and stdout is not a terminal; -no-agent turns it off, .help agent explains the output

The line states why the mode is on and how to turn it off.

The first version printed three lines of explanation (634 bytes) on every run. That was larger than most query results, and a model keeps its context between runs anyway, so it only needs the explanation once.

The longer text is available via .help agent. It describes the output rules and settings, and it points to a few features an agent might not find by itself:

  • SET max_execution_time = <ms> to put an upper bound on a query’s runtime
  • DESCRIBE <query> to get the result columns without running the query
  • SUMMARIZE for a quick profile of a table or query
  • .tables for the schema
  • duckdb_functions() for function documentation

If you do not want the startup line, add .startup_text none to your ~/.duckdbrc.

#

Trying It Out

If you use a coding agent, there is nothing to configure. Agents such as Claude Code, Codex, Cursor, Gemini CLI and GitHub Copilot already set one of the environment variables from the table above, and they capture a command's output through a pipe rather than a terminal. Those are two of the three conditions for agent mode.

So as long as the duckdb binary on the agent's path is v2.0 or newer and the agent does not request a specific output format with a flag such as -csv, -json or -markdown, the mode turns on by itself. A format flag is treated as a deliberate choice, so it switches agent mode off: the CLI assumes you want exactly that format and does not override it.

To see what the agent sees, run a query through a pipe with the flag set:

duckdb -agent -c "SUMMARIZE FROM 'my_data.parquet'" | cat

#

Conclusion

With agent mode, an agent spends fewer tokens per call and does not act on partial results as if they were complete. That means fewer steps to a correct result.

The current behavior is a first attempt, and the conventions around agents are a work in progress. If your agent is not detected, or the output confuses it, please open an issue or comment on the pull request. Transcripts of an agent being confused are especially useful.

#

Further Reading

── more in #ai-agents 4 stories Β· sorted by recency
── more on @duckdb 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/agent-mode-in-the-du…] indexed:0 read:11min 2026-10-09 Β· β€”