Field note

Notes on Intuit's QuickBooks CLI

A technical audit of intuit-cli: setup quirks, unvalidated query interpolation, exit code 127 crashes on Windows, and why --dry-run covers 3 of 36 write verbs.

Technical inspection of CLI tooling and API query layers.

Intuit began publishing intuit-cli in April 2026. Version 0.2.4 landed in July. It is an official command-line client for the QuickBooks Online API, and since we spend most of our time driving that API, we installed it and worked through the surface. These are our technical notes.

Tested on 28 August 2026: intuit-cli@0.2.4, Node 24.18, Windows 11, authenticated against an Intuit sandbox company. No client file was touched, and the only write attempted was a --dry-run.

Setup

npm install -g intuit-cli
intuit auth login --profile sandbox --env sandbox

Two things to know before you start:

The sandbox redirect URI is hardcoded to http://localhost:9477/callback, so you have to add it to your Intuit developer app settings before the login will complete. Production requires a registered HTTPS URI: localhost is rejected there.

Credentials come from a .env file resolved against the current working directory (import "dotenv/config" at dist/cli.js:2). Tokens themselves go somewhere sane: ~/.config/intuit-cli/, AES-256-GCM with OS keychain backing. However, the client ID and secret are working-directory dependent, so the same command run from two different directories can hit two different companies. That is worth knowing if you script it.

The environment variables are INTUIT_SANDBOX_CLIENT_ID / INTUIT_SANDBOX_CLIENT_SECRET and their INTUIT_PROD_ equivalents. Login prints an authorization URL, listens on port 9477, and writes a named profile when the callback lands.

What works

The read side is solid. auth status prints a table showing not just the state but the source of every setting (which file or environment variable each value came from), which is a clearer diagnostic than most CLIs provide:

Profile        zeno-sandbox (active)                     profiles.json
Credentials    Configured                                INTUIT_SANDBOX_CLIENT_* env vars
Access Token   Valid (60m remaining)                     ~/.config/intuit-cli/...

Twenty entity nouns follow a consistent intuit <noun> <verb> grammar. --all auto-paginates. --csv dumps every raw column including sync tokens and metadata timestamps, with nested references JSON-escaped inline. Named profiles handle multi-company work, with --profile as a per-command override.

intuit query passes a statement straight to the QuickBooks Online query endpoint and returns raw QueryResponse JSON. For answering an ad-hoc question about a file without writing ad-hoc script code, it is fast.

The webhooks guide, listen, and replay commands also provide a clean local development workflow for testing inbound event webhooks.

--where is an unvalidated passthrough

The README’s own example fails:

$ intuit customers list --where "Balance > 0"
Error: 400 Error parsing query — QueryParserError: Encountered " ">" "> ""
at line 1, column 38.

With --debug enabled, you can see why: the clause is interpolated verbatim into the query string, and QuickBooks Online’s parser does not accept > on Balance. The operators listed as valid by the API are =, !=, like, in, between, and contains.

So --where is not a filter abstraction; it is a raw string splice. You need to know the QuickBooks Online SQL dialect to use it. If you keep to supported syntax, --where "DisplayName LIKE 'A%'" works fine.

API errors abort the process on Windows

Every error returned by the Intuit API (a bad ID, a malformed query) aborts with a libuv assertion rather than exiting cleanly:

Error: Intuit API error: 400 ... intuit_tid: 1-6a91d2e9-23b60b8857e1b64e3d3d6395
Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 94

Reproduced 3 out of 3 times. The exit code is 127, not 1.

That distinction matters if you wrap the CLI in scripts. Exit code 127 conventionally signals “command not found”, so a parent process cannot distinguish an API validation failure from a missing binary. Local errors (bad profile name, unauthenticated state) exit cleanly with 1, so the crash is specific to response handling from Intuit’s endpoints. We only observed this on Windows; the likely culprit is the native @napi-rs/keyring handle tearing down mid-close.

An unrelated version detail: intuit --version reports 0.1.0. It is hardcoded at dist/cli.js:118 while the published npm package is 0.2.4.

--dry-run covers 3 of 36 write verbs

This is the most significant architectural observation, and it is not a bug.

--dry-run prints the outbound request payload without sending it:

$ intuit customers create --display-name "Acme" --idempotency-tag run-b3a7 --dry-run
[dry-run] POST /customer
{
  "DisplayName": "Acme",
  "Notes": "[via Intuit CLI · run run-b3a7]"
}

Counting every create, update, void, and delete subcommand that actually exists across the CLI’s entity commands, there are 36 write verbs. Only three accept --dry-run: customers create, vendors create, and invoices create. --idempotency-tag has the exact same three-command footprint, appending its marker to the record’s Notes field.

All three supported verbs are creates. Every update, void, and delete command posts directly without a preview option.

That is a coherent design choice for a developer tool. --dry-run answers: did I construct the payload I intended to construct?, which is a helpful debugging check when building a create script against a sandbox. It was not intended to be a human approval layer.

However, it does mean previews are provided only for the operation that is easiest to undo (creating a new draft record), and entirely absent from the operations that modify existing records or remove them. If you point the CLI at a production company file, update and delete calls post immediately without confirmation or attribution tags. Whatever review checks you need around those actions must be built outside the tool.

Intuit CLI write verbs: only 3 of 36 write subcommands support dry-run previews and idempotency tags

Reproducing the verb count

To verify the subcommand count independently:

for n in customers vendors invoices bills payments items estimates purchases \
         deposits salesreceipts creditmemos billpayments employees; do
  verbs=$(intuit $n --help | sed -n '/Commands:/,$p' | awk '{print $1}' \
          | grep -E '^(create|update|void|delete)$')
  for v in $verbs; do
    intuit $n $v --help | grep -q -- '--dry-run' && echo "$n $v: dry-run"
  done
done

(Note: a naive loop over help output will report 52 verbs instead of 36 because running intuit <noun> void --help on entities that lack void falls back to displaying parent help. Parsing the Commands: block explicitly avoids this.)

Summary

intuit-cli is a capable first-party tool for developers, and the read side (query, --csv, auto-pagination, named profiles) is immediately useful for inspecting sandbox company data. The exit-code crash on Windows is the primary issue to guard against if automating around it today.

SEE THE WORKFLOW

Put one real batch through human review.

Explore Zeno for Firms