- Go 95.9%
- Shell 3.8%
- Makefile 0.3%
Fixes the completion install broken in v0.4.0. Cuts **v0.4.1**. ## Bug v0.4.0's cask used `shell_parameter_format: arg`, so Homebrew invoked `things completions --shell=bash`. But `things completions` takes the shell as a **positional** arg, so Kong rejected the unknown `--shell` flag (exit 80) and `brew install` wrote no completion files — with three "Failed to generate … completions" warnings. (The preflight quarantine fix from #95 worked — the binary *executed*; exit 80 is our own arg-parse error, not a Gatekeeper block.) ## Fix Drop `shell_parameter_format` so Homebrew uses its default: a bare positional arg, `things completions bash`. The GoReleaser docs claim `arg` means positional, but Homebrew's `:arg` actually emits `--shell=<name>`; bare positional is the no-format default. Generated cask now reads: ```ruby generate_completions_from_executable "things", "completions", base_name: "things", shells: [:bash, :zsh, :fish] ``` ## Verification - `goreleaser check` ✅; snapshot cask omits `shell_parameter_format` ✅ - Against the **installed v0.4.0 binary**: `things completions --shell=bash` → exit 80 (reproduces the failure); `things completions bash` → exit 0 emitting `complete -C things things`; all of bash/zsh/fish exit 0 positionally ✅ After v0.4.1 releases: `brew update && brew reinstall ryanlewis/tap/things` should write the completion files with no warnings. |
||
|---|---|---|
| .claude | ||
| .github | ||
| cmd/things | ||
| docs | ||
| internal | ||
| scripts | ||
| .gitignore | ||
| .golangci.yml | ||
| .goreleaser.yaml | ||
| CLAUDE.md | ||
| go.mod | ||
| go.sum | ||
| install.sh | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| renovate.json | ||
things-cli
A small Go CLI for Things3 on macOS. Reads
tasks, projects, areas and tags straight from the Things3 SQLite database
(read-only) and writes via the things:/// URL scheme and AppleScript — so the
app stays the source of truth and your data never leaves the machine.
things # today's tasks
things inbox -j | jq # JSON for piping into anything
things add "Buy milk" --when today --tags errand
things edit 3 --add-tags urgent --deadline 2026-05-01
things complete "Pay rent" # by title, with interactive disambig
things search migrate # full-text across titles + notes
things open --project "Launch" # reveal in the Things app
What it does:
- List & inspect every built-in view (
today,inbox,upcoming,anytime,someday,logbook,trash,deadlines) plus projects, areas, tags, and full-text search - Create tasks and projects with notes, schedules, deadlines, tags, checklists, and headings
- Edit anything mutable via
things:///update— only the flags you pass are sent, so unset fields stay untouched - Complete, cancel, log, or reveal items in the app
- Import Things JSON URL scheme payloads in bulk
- JSON everywhere — every command supports
-j/--jsonfor clean piping intojq, agents, or scripts
Teach your agent to drive it. A bundled skill ships in the binary —
install it once and your agent knows when to reach for things instead
of guessing at AppleScript:
things skill install claude # also: codex, pi
For other agents, things skill show prints the neutral source so you can
append it to whatever your agent reads for instructions (e.g. a project
AGENTS.md).
CLI
By default output is plain text formatted for humans. Pass -j / --json
for structured JSON suitable for piping into jq or another tool. List
commands assign each result a stable index (1, 2, 3, …) you can use
in follow-up commands like show, edit, complete, and cancel.
Global flags
| Flag | Description | Default |
|---|---|---|
-j, --json |
Output as JSON instead of plain text | false |
--db PATH |
Override the Things3 SQLite database path | auto-detected |
-v, --version |
Print version, commit, and build date and exit (same as things version) |
— |
Listing tasks
things <view> prints a built-in list. With no arguments, things prints
today. View names take precedence over project names — a non-view
argument is treated as a project name (things "Weekly Review"), so a
project literally called Inbox would need things -p Inbox.
| View | Description |
|---|---|
today |
Tasks scheduled for today (default) |
inbox |
Inbox |
upcoming |
Scheduled tasks and deadlines |
anytime |
Anytime list |
someday |
Someday list |
logbook |
Completed tasks |
trash |
Trashed tasks |
deadlines |
Tasks with a deadline |
Filters (combine freely with any view):
| Flag | Description |
|---|---|
-p, --project NAME |
Filter by project name or UUID |
-a, --area NAME |
Filter by area name or UUID |
-t, --tag NAME |
Filter by tag name |
Examples:
things # today (default)
things inbox
things upcoming -t urgent
things "Weekly Review" # tasks in a project by name
things -a Work # tasks in an area
Output groups by project or area; numeric indices are stable for follow-up commands until the next listing:
$ things
Launch v2
1. [ ] Draft release notes [docs] today
2. [ ] Cut RC build due:2026-04-30
Errands
3. [ ] Buy milk [shopping]
4. [x] Pick up dry cleaning
Inspecting tasks, projects, areas, tags
| Command | Description |
|---|---|
things show <task> |
Show a task's detail (with checklist) |
things projects [--area NAME] [--completed] |
List projects |
things areas |
List areas |
things tags |
List tags |
things search <query> |
Full-text search across titles and notes |
<task> accepts a UUID, a numeric index from the last list, or a title
substring. When a title matches multiple tasks, an interactive prompt picks
between them; non-TTY callers get the match list as an error.
things show 3 # task #3 from the last list
things show "Pay rent" # by title (interactive disambig)
things search migrate # full-text search
$ things show 2
Title: Cut RC build
UUID: 8K3FpQ2eRtNbHwpNiM71Eu
Status: Open
Project: Launch v2
Tags: release
Deadline: 2026-04-30
Created: 2026-04-12 09:14
Notes:
Coordinate with marketing before tagging.
Checklist:
[x] Bump version
[ ] Update changelog
[ ] Tag and push
things projects renders a one-line-per-project list; the leading glyph
shows completion progress (○ empty, ◔ ◑ ◕ partial, ● done, ◌
cancelled):
$ things projects
◑ Launch v2 Work
◔ Migrate API Work
○ Garden plan Home
● Spring cleaning Home
Creating tasks and projects
things add <title> creates a task. things project add <title> creates a
project.
| Flag | add |
project add |
Description |
|---|---|---|---|
--notes TEXT |
✓ | ✓ | Free-form notes |
--when VALUE |
✓ | ✓ | Schedule (see Date values) |
--deadline DATE |
✓ | ✓ | Deadline date |
--tags LIST |
✓ | ✓ | Comma-separated tags |
--checklist ITEMS |
✓ | — | Newline-separated checklist items |
--todos ITEMS |
— | ✓ | Newline-separated initial to-dos |
--project NAME |
✓ | — | Project to add the task into |
--heading NAME |
✓ | — | Heading within the project |
--list NAME |
✓ | — | List (project or area) name |
--area NAME |
— | ✓ | Area to file the project under |
Examples:
things add "Buy milk" --when today --tags errand,shopping
things add "Ship v2" --project "Launch" --deadline 2026-04-30
things project add "Launch site" --area Work --deadline 2026-05-01
Editing tasks and projects
things edit <task> updates a task via things:///update.
things project edit <project> updates a project via
things:///update-project. Only the flags you pass are sent — unset fields
stay untouched. An empty value clears the field (e.g. --deadline "").
Prerequisite:
edit,project edit, andimportpayloads withoperation: updaterequire the Things auth token. Enable it once via Things → Settings → General → Enable Things URLs. Without it, writes fail withupdate: auth token is required — enable Things URLs in Things → Settings → General ….
| Flag | edit |
project edit |
Description |
|---|---|---|---|
--title TEXT |
✓ | ✓ | Replace title |
--notes TEXT |
✓ | ✓ | Replace notes |
--prepend-notes TEXT |
✓ | ✓ | Prepend to notes |
--append-notes TEXT |
✓ | ✓ | Append to notes |
--when VALUE |
✓ | ✓ | Reschedule (see Date values) |
--deadline DATE |
✓ | ✓ | Set deadline |
--tags LIST |
✓ | ✓ | Replace all tags (comma-separated) |
--add-tags LIST |
✓ | ✓ | Add tags without replacing existing |
--checklist ITEMS |
✓ | — | Replace checklist (newline-separated) |
--prepend-checklist ITEMS |
✓ | — | Prepend checklist items |
--append-checklist ITEMS |
✓ | — | Append checklist items |
--list NAME |
✓ | — | Move to list/project by name |
--list-id UUID |
✓ | — | Move to list/project by UUID |
--heading NAME |
✓ | — | Set heading within project by name |
--heading-id UUID |
✓ | — | Set heading within project by UUID |
--area NAME |
— | ✓ | Move project to area by name |
--area-id UUID |
— | ✓ | Move project to area by UUID |
--complete |
✓ | ✓ | Mark as completed |
--cancel |
✓ | ✓ | Mark as canceled |
--duplicate |
✓ | ✓ | Duplicate before applying edits |
--reveal |
✓ | ✓ | Reveal in Things after editing |
Examples:
things edit 3 --title "New title" --when tomorrow
things edit "Buy milk" --add-tags urgent --deadline 2026-05-01
things edit "Old idea" --deadline "" # clear the deadline
things project edit "Launch" --append-notes "Beta cut on Friday"
Completing, cancelling, logging
| Command | Description |
|---|---|
things complete <task> |
Mark a task or project as completed (project completion is confirmed interactively) |
things cancel <task> |
Cancel a task |
things log |
Move today's done/cancelled items to the Logbook (Items → Log Completed) |
log is the housekeeping action; logbook (above) is the view of
already-archived tasks.
things complete 3
things cancel "Old idea"
things log
Revealing items in Things3
things open brings Things3 forward and reveals a list, item, or quick-find
result. Pass exactly one of:
| Flag / Argument | Description |
|---|---|
<ref> |
Built-in list name (today, inbox, …), task UUID, numeric list index, or title |
-p, --project NAME |
Open a project by name or UUID |
-a, --area NAME |
Open an area by name or UUID |
-t, --tag NAME |
Open a tag by name or UUID |
-q, --query TEXT |
App-side quick find |
Additional flags:
| Flag | Description |
|---|---|
--filter TAGS |
Tag filter on the shown list (comma-separated) |
--background |
Don't bring Things to the foreground |
Examples:
things open today
things open "Pay rent"
things open --project "Launch"
things open --query staging
Importing JSON payloads
things import forwards a Things JSON URL scheme
payload — a
batch of to-do, project, heading, and checklist-item items, each
with operation and attributes. The CLI validates the payload is
syntactically valid JSON, then forwards it verbatim. The auth token is
attached automatically (required for operation: update items, harmless
for create-only payloads).
| Flag | Description |
|---|---|
-f, --file PATH |
Read JSON payload from this file instead of stdin |
--reveal |
Reveal the first created/updated item in Things after import |
things import < payload.json
things import --file payload.json --reveal
Note: macOS open has a URL length limit; split very large payloads.
Date values
--when accepts:
| Form | Example |
|---|---|
| Keyword | today, tomorrow, evening, anytime, someday |
| Date | 2026-05-01 |
| Time | HH:MM (21:30) or H:MMam / H:MMpm (9:30PM) |
| Date + time | 2026-05-01@09:30 |
| RFC3339 | 2026-05-01T09:30:00Z (rewritten to YYYY-MM-DD@HH:MM; offset preserved as wall-clock, no conversion to local time) |
| Natural language | friday, next monday (English locales only; passed through verbatim) |
Inputs within edit distance 2 of a known keyword are rejected client-side
as likely typos (e.g. tommorrow, evning) with a "did you mean" hint.
--deadline accepts a YYYY-MM-DD date or an English natural-language
phrase. Keywords are not accepted.
project add accepts --notes, --when, --deadline, --tags, --area
and --todos (newline-separated initial to-dos).
import accepts a JSON array on stdin (or via --file) matching the
Things JSON URL scheme payload
— a batch of to-do, project, heading, and checklist-item items, each
with operation and attributes. The CLI validates the payload is
syntactically valid JSON, then forwards it verbatim. The auth token is
attached automatically (required for operation: update items, harmless for
create-only payloads). Pass --reveal to jump to the first created item.
Note: macOS open has a URL length limit; split very large payloads.
project edit updates an existing project via the things:///update-project
URL scheme. Only flags you pass are sent. Supported flags: --title,
--notes, --prepend-notes, --append-notes, --when, --deadline,
--tags (replace), --add-tags, --area / --area-id, --complete,
--cancel, --duplicate, --reveal. An empty value clears the field
(e.g. --deadline ""). Requires the Things auth token, same as edit.
edit updates an existing task via the things:///update URL scheme. Only
flags you pass are sent, so unset fields stay untouched. Supported flags:
--title, --notes, --prepend-notes, --append-notes, --when,
--deadline, --tags (replace), --add-tags, --checklist,
--prepend-checklist, --append-checklist, --list / --list-id,
--heading / --heading-id, --complete, --cancel, --duplicate,
--reveal. An empty value clears the field (e.g. --deadline ""). Requires
the Things auth token — enable Things → Settings → General → Enable Things
URLs.
Shell completions
things completions <bash|zsh|fish> prints a completion script for the named
shell. The script delegates back to things at completion time, so it never
goes stale as the command surface changes — things <TAB> completes
subcommands and flag names, and a flag's values complete once you've typed it
(things list --color <TAB> → auto, always, never).
The Homebrew cask generates these on install, so cask users get things <TAB>
with no extra steps. On every other install path, load the script yourself.
Completion shells out to things by name, so it works as long as things is on
your PATH (the Homebrew, go install, and make install paths all put it
there):
# bash — add to ~/.bashrc (complete -C is a bash builtin; no extra package needed)
source <(things completions bash)
# zsh — add to ~/.zshrc, after compinit runs (the stub's bashcompinit needs compdef)
source <(things completions zsh)
# fish — load now, and/or persist for new shells
things completions fish | source
mkdir -p ~/.config/fish/completions
things completions fish > ~/.config/fish/completions/things.fish
Completion runs entirely from the static command tree — it never reads the Things database, so project, area, and tag names are not (yet) completed.
Agent skill
things-cli bundles an agent skill that teaches Claude Code, OpenAI's Codex
CLI, the Pi coding agent, and other compatible agents how to drive the CLI.
Install it once and the agent will know when to reach for things instead
of guessing.
| Command | Description |
|---|---|
things skill list |
Show supported agents and install status |
things skill install <agent> |
Install the skill for an agent (claude, codex, pi) |
things skill uninstall <agent> |
Remove the installed skill |
things skill show |
Print the neutral skill source |
things skill show <agent> |
Print the files that would be installed for that agent |
Default install paths:
| Agent | Path |
|---|---|
claude |
~/.claude/skills/things-cli/ |
codex |
~/.codex/skills/things-cli/ |
pi |
~/.pi/agent/skills/things-cli/ |
install and uninstall accept:
| Flag | Description |
|---|---|
--path DIR |
Install or uninstall under a custom directory (e.g. project-local .claude/skills/ or .agents/skills/) |
-y, --yes |
Skip the overwrite/removal prompt |
The skill body is internal/skill/SKILL.md,
embedded in the binary — so a plain things upgrade refreshes it; re-run
skill install to pick up the new version.
How it works
- Reads go through
modernc.org/sqlite(pure Go, no cgo) withPRAGMA query_only = ON, so the CLI cannot mutate the Things database. - Writes go through the official
things:///addandthings:///updateURL schemes for creating and editing tasks, and through AppleScript for completing and cancelling them. This is the same interface Things exposes to Shortcuts and automation tools. - Task resolution accepts a UUID, a title (with interactive disambiguation when multiple tasks match) or a numeric index into the last listing.
Install
With Homebrew:
brew install ryanlewis/tap/things
Or one-line install (downloads the latest release, verifies checksums,
installs to /usr/local/bin):
curl -fsSL https://raw.githubusercontent.com/ryanlewis/things-cli/main/install.sh | sh
Override the destination with INSTALL_DIR or pin a version with VERSION:
curl -fsSL https://raw.githubusercontent.com/ryanlewis/things-cli/main/install.sh \
| INSTALL_DIR="$HOME/bin" VERSION=v0.1.0 sh
Or download a prebuilt binary manually from the
latest release
(darwin_arm64 for Apple Silicon, darwin_amd64 for Intel):
tar -xzf things_*_darwin_arm64.tar.gz
mv things /usr/local/bin/ # or ~/bin, etc.
things version
Or install with go install:
go install github.com/ryanlewis/things-cli/cmd/things@latest
Or build from source:
make build # produces ./things
# or
go build -o things ./cmd/things
Requires macOS with Things3 installed. Go 1.26 or later when building from source.
Project structure
cmd/things/ CLI entry point (alecthomas/kong)
internal/model/ Shared types + date codecs (ThingsDate, Core Data time)
internal/db/ SQLite queries, read-only
internal/things/ URL scheme + AppleScript writers
internal/output/ JSON and plain-text rendering
internal/cache/ Last-list UUID cache for numeric references