Docs
Reference

Configuration

Every flag things accepts has a built-in default. A TOML config file lets you change those defaults once instead of typing the same flag on every run — colour always on, say, or a database somewhere other than where the CLI would look.

The file only supplies defaults. A flag on the command line still wins, so nothing you put here can take an option away from you.

Where the file lives

things uses the first of these it finds:

OrderSourceNotes
1--config PATHWins over everything else, for one run
2$THINGS_CLI_CONFIGSame, for a whole shell session
3$XDG_CONFIG_HOME/things-cli/config.tomlOnly when XDG_CONFIG_HOME is set
4~/.config/things-cli/config.tomlThe default
The config file is optional. Without one, things runs on its built-in defaults. The same applies to a path you name yourself; run things config path to see which file is in use and whether it exists.

~ and relative paths are expanded, so --config ./project.toml works.

Precedence

flag on the command line  >  config file  >  built-in default

There is no second code path: the file is a resolver the CLI hands to its argument parser, which consults it only for flags you did not pass. That is what makes the rule hold everywhere without exception.

# config file says color = "always"
things today                  # colour on   — from the file
things --color never today    # colour off  — the flag wins

For a boolean, spell the override out to turn it back off:

things --json=false today     # even with json = true in the file

Keys

Keys are named after the flag they set, in snake_case — except assume_yes, which is deliberately narrower than the --yes it seeds (see below). The flag’s own spelling is accepted too, so no-verify works as well as no_verify — but pick one: setting the same option under both spellings in one file is an error rather than a coin toss.

KeyAlso acceptedTypeDefaultApplies toWhat it does
jsonbooleanfalseevery commandPrint JSON instead of the plain text listing
color"auto" | "always" | "never""auto"every commandWhen to colour output; auto means only on a terminal
hintsbooleantrueevery commandPrint the hint line under a plain task listing
dbpathauto-detectedevery commandWhere the Things3 SQLite database is; the file must exist
no_verifyno-verifybooleanfalsecomplete, cancel, edit, project edit, import, tag add (and any write that creates tags)Skip the read-back that confirms a status change or a tag creation landed
strict_tagsstrict-tagsbooleanfalseadd, edit, project add, project edit, importFail instead of writing when a tag does not exist
create_tagscreate-tagsbooleanfalseadd, edit, project add, project edit, importCreate missing tags before writing
assume_yesyesbooleanfalsecomplete, cancelAnswer the confirmation before a project-wide status change

Two constraints the CLI enforces:

  • strict_tags and create_tags are mutually exclusive. Setting both to true is an error, reported as soon as the file is read — they are two different answers to the same question.
  • db must point at a file that exists. The check runs when a command opens the database, so a stale path is reported against the config file rather than as a puzzling flag error — and the commands that never read the database keep working, which is how you find out the path is stale.

strict_tags and create_tags also override each other from the command line: --create-tags on a run whose file says strict_tags = true is the override it looks like, not a “can’t be used together” error.

A short file setting two things:

color = "always"
assume_yes = true

An example file

things config init writes this template, with every key present and commented out. Uncomment a line to change that default.

# things-cli configuration
#
# Defaults for the flags below. A flag passed on the command line always
# wins: flag > this file > built-in default.
#
# Read from $XDG_CONFIG_HOME/things-cli/config.toml, or
# ~/.config/things-cli/config.toml. Override with --config PATH or
# $THINGS_CLI_CONFIG. The file is optional; without it, things runs on
# its built-in defaults.
#
# Uncomment a line to change the default.

# Print JSON instead of the plain text listing. Same as --json / -j.
# Turning this on changes the output of every command, including for
# agents and scripts that expect plain text.
# json = false

# Color mode: "auto", "always" or "never". Same as --color.
# "auto" colours only when stdout is a terminal.
# color = "auto"

# Print the hint line under a plain task listing. Same as --hints /
# --no-hints. The hint only ever appears when stdout is a terminal and
# the output is not JSON, so turning it off is for terminal use.
# hints = true

# Path to the Things3 SQLite database. Same as --db.
# Leave unset to let things-cli find it. The file must exist.
# db = "~/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/Things Database.thingsdatabase/main.sqlite"

# Skip the read-back that confirms a complete/cancel actually landed.
# Same as --no-verify. Faster, but a write Things silently drops is
# then reported as a success.
# no_verify = false

# Fail instead of writing when a tag does not exist in Things.
# Same as --strict-tags. Off by default, which warns and writes anyway.
# Mutually exclusive with create_tags.
# strict_tags = false

# Create tags that do not exist in Things before writing.
# Same as --create-tags. Mutually exclusive with strict_tags.
# create_tags = false

# Answer the confirmation asked before a project-wide complete or
# cancel, instead of prompting. Same as --yes / -y on those two
# commands; the --yes on `skill install` and `skill uninstall` is
# deliberately not covered, so this cannot delete an installed skill.
# Turning this on removes the last check before a project and every
# task in it changes status.
# assume_yes = false
This block is checked against the real template by a test in the repository, so it cannot drift from what things config init writes.

The config commands

things config path

Prints the file in use and whether it is there.

$ things config path
/Users/me/.config/things-cli/config.toml (exists)

--json adds where the path came from — flag, env, or default:

$ things --json config path
{
  "path": "/Users/me/.config/things-cli/config.toml",
  "exists": true,
  "source": "default"
}

things config show

Prints the default each key resolves to and whether that came from the file or the CLI. These are the values that apply when you pass no flag — flags you pass to config show itself do not appear here.

$ things config show
config: /Users/me/.config/things-cli/config.toml (exists)
These apply when no flag overrides them.

  json         false    default
  color        always   config
  hints        true     default
  db           (unset)  default
  no_verify    false    default
  strict_tags  false    default
  create_tags  false    default
  assume_yes   true     config

--json gives the same thing as a settings array — one entry per key, in the order above, abridged here:

$ things --json config show
{
  "path": "/Users/me/.config/things-cli/config.toml",
  "exists": true,
  "source": "default",
  "settings": [
    {
      "key": "json",
      "value": false,
      "source": "default"
    },
    {
      "key": "color",
      "value": "always",
      "source": "config"
    }
  ]
}

things config init

Writes the annotated template above, creating the directory if it is not there. It refuses to overwrite a file that already exists:

$ things config init
Wrote config template to /Users/me/.config/things-cli/config.toml

$ things config init
Error: config file already exists: /Users/me/.config/things-cli/config.toml — pass --force to overwrite

Pass --force to replace it, or --config PATH to write somewhere else.

When the file is wrong

A problem with the file is reported on its own line, naming the file and what is wrong with it. It is not a usage mistake, so there is no usage dump. The command exits 2.

$ things config path
Error: config file /Users/me/.config/things-cli/config.toml: unknown key "verbose" (valid keys: json, color, hints, db, no_verify, strict_tags, create_tags, assume_yes)
$ things config path
Error: config file /Users/me/.config/things-cli/config.toml: key "no_verify" is set twice ("no_verify" and "no-verify" name the same setting)
$ things config path
Error: config file /Users/me/.config/things-cli/config.toml: invalid TOML: line 1, column 16: basic strings cannot have new lines
$ things config path
Error: config file /Users/me/.config/things-cli/config.toml: key "color" must be one of auto, always, never, got "pink"

A db path that no longer exists is caught when a command opens the database, not before, so the commands that never read it still run — and a --db on the command line still overrides a stale entry rather than tripping over it:

$ things today
Error: config file /Users/me/.config/things-cli/config.toml: db: stat /Users/me/old/main.sqlite: no such file or directory

$ things config path                        # tells you which file to fix
/Users/me/.config/things-cli/config.toml (exists)

$ things --db ~/current/main.sqlite today   # works

The diagnostic commands always run

A file the CLI cannot use stops every command that would have read it, but not the ones that exist to tell you about it. things config path, things config show, things config init and --help all work against a broken file:

$ things config path
/Users/me/.config/things-cli/config.toml (exists)
warning: this file cannot be used: config file /Users/me/.config/things-cli/config.toml: invalid TOML: line 1, column 16: basic strings cannot have new lines

config path still exits 0 — which file is in use is a fact about the path, not its contents — and puts the warning on stderr. config show names the file and then reports the problem, because there are no values to show:

$ things config show
config: /Users/me/.config/things-cli/config.toml (exists)
Error: config file /Users/me/.config/things-cli/config.toml: invalid TOML: line 1, column 16: basic strings cannot have new lines

config init refuses to overwrite as usual, but says why the file is unusable, and --force replaces it with a fresh template:

$ things config init
Error: config file already exists: /Users/me/.config/things-cli/config.toml (unusable as it stands: invalid TOML: line 1, column 16: basic strings cannot have new lines) — pass --force to overwrite

$ things config init --force
Wrote config template to /Users/me/.config/things-cli/config.toml

Under --json these come out as a JSON object on stdout like any other failure, so a script sees a structured error either way.

assume_yes and the skill commands

Completing or cancelling a project also completes or cancels every task inside it, so things complete asks first. A run that cannot prompt — stdin is not a terminal (cron, a CI job, < /dev/null), or --json, which never prompts whatever stdin is — declines. assume_yes answers that question ahead of time, which is what lets a script or an agent complete a project.

Piping output does not turn the prompt off: things complete "Roof" | tee log still asks on stderr and reads the answer from the terminal.

--yes also exists on skill install and skill uninstall, where it means “overwrite” and “delete”. assume_yes deliberately does not reach those: a file written so a script can tick off a project should not also let things skill uninstall remove files without asking. Those two still prompt, or take the flag directly.

things skill uninstall claude --yes   # the flag works
                                      # assume_yes = true does not do this for you

Keys can be scoped this way in general — assume_yes is the only one that currently is.

Notes for agents and scripts

A config file can change defaults you would otherwise assume. json = true makes every command emit JSON; no_verify = true turns off the read-back that confirms a complete landed; hints = false drops the hint line.

If you are writing something that has to behave the same on any machine, pass the flags you depend on rather than inheriting them:

things --json=false today     # plain text, whatever the file says
things --json list today

things config show reports the file in use and everything it sets, which is the quickest way to find out why a command on one machine behaves differently from the same command on another.