README
¶
taiga-cli
What it is
taiga is a command line client for the Taiga REST API v1, built for people and for coding agents. It resolves the Taiga URL and project from flags, environment, a .taiga.toml in the repository or the user config, keeps a session cache so agents never handle passwords, and reports every failure as a stable JSON error envelope with an exit code.
taiga api reaches the whole API v1, including version-checked writes and transparent pagination, so anything without a curated command is still one call away. Licensed under Apache-2.0.
Install
go install github.com/BasisTI/taiga-cli/cmd/taiga@latest
Or download the tarball for your platform from GitHub Releases, together with SHA256SUMS, and check it:
sha256sum -c SHA256SUMS --ignore-missing
tar -xzf taiga_*_linux_amd64.tar.gz taiga && sudo install taiga /usr/local/bin/
Quickstart
taiga auth login --url https://taiga.example.com --username me
echo 'url = "https://taiga.example.com"' > .taiga.toml
echo 'project = "my-project"' >> .taiga.toml
taiga api GET users/me
taiga api GET userstories --query project=37 --paginate
taiga api PATCH userstories/123 --field comment="Deployed to staging" --auto-version
Stories
taiga story works on the user stories of the selected project (--project, TAIGA_PROJECT, .taiga.toml or the config). A story is named by its reference (REF, the number shown in the web UI), never by its internal id; story get --id takes the id and still checks the project.
| Command | What it does |
|---|---|
taiga story list [--ref N] [--status S] [--assignee USER|me] [--epic REF] [--tag T]... [--search TEXT] [--closed[=false]] |
Every matching story, without manual paging. Repeated --tag must all match |
taiga story get REF / taiga story get --id ID |
One story, with its web url; JSON output also carries custom_attributes (values with their own version) |
taiga story create --subject S [--description-file F|-] [--status S] [--tag T]... [--swimlane L] [--assignee USER]... |
Create a story |
taiga story update REF [--subject S] [--description-file F|-] [--append-description TEXT] [--status S] [--tag T]... [--add-tag T]... [--remove-tag T]... [--milestone M] [--swimlane L] [--add-assignee USER]... [--remove-assignee USER]... [--owner-assignee USER|--clear-owner-assignee] [--block NOTE|--unblock] |
Send only the fields that change, with the story version |
taiga story close REF [--status S] |
Move to a closed status; never archives or deletes |
taiga story field list REF |
The story's custom field values next to their definitions |
taiga story field set REF ["Name=value"]... [--unset NAME]... |
Merge custom field values: named fields change, the others stay |
taiga story comment REF --body TEXT|--body-file F|- |
Publish a comment (Markdown); sent once, never repeated |
taiga story comments REF [--include-system] |
The story's comments, newest first |
- Statuses, milestones and swimlanes take a name or an id of the project; users take an exact username, an id or
me, and must be project members. A name used twice isambiguous_name. --tagreplaces the tags;--add-tag/--remove-tagmerge with the current ones. Taiga stores tags in lower case, so the CLI sends them that way.--add-assignee/--remove-assigneemerge with the current assignees (assigned_users) and never change the main assignee (assigned_to); that takes--owner-assigneeor--clear-owner-assignee. Taiga always shows the main assignee among the assignees, so removing them needs one of those two flags in the same command. Changing the main assignee keeps the others.--remove-assigneealso takes users who left the project. A write that changes assignees is not retried after a version conflict (exit 4; run it again, or use--force-version).- Taiga's concurrency check does not cover the main assignee (
assigned_to). The CLI re-reads the assignees right before writing (a change since the first read isversion_conflict) and checks the story after writing; a mismatch isassignees_postcondition_failed(exit 4): the write was applied, so check the story instead of re-running. A concurrent change in the short window between that re-read and the write is detected, not prevented. See docs/api-notes.md. --block NOTEsetsis_blockedwith the note (required);--unblockclears both.closewithout--statususes the project's only closed status; with several (the default template hasDoneandArchived) it asks for one.- Every write accepts
--dry-run(prints method, path and body) and, exceptcreate,--force-version. commentsends the text exactly as given (quotes, accents, line breaks) and never touches the description. A blank comment is refused. Taiga accepts any pastversionfor a comment, so the version protects nothing there and there is no--force-version: the comment is sent once. If the answer is lost (network error or 5xx), the CLI looks for it in the story history: found, the command succeeds; otherwise it exits withcomment_unconfirmed(exit 1), because a request still running on the server can land later. Checkstory commentsbefore publishing again. Exit 7 (network_error) means the connection never opened, so nothing was sent.commentslists every comment, including edited and deleted ones (edit_comment_date,delete_comment_date). Comments written by Taiga's own GitLab integration (its inactivegitlab-<hash>user, with the push hook templates "This user story has been mentioned by …" and "… changed the status from [GitLab commit]…") are hidden unless--include-system; every other author, service accounts included, is always shown. Each entry carriesis_system,story_refand the storyurl.--epiconcreate/updateanswersunsupported_operation: Taiga links epics through a separate, unversioned resource whose replacement needsDELETE(see docs/api-notes.md).
taiga story list --assignee me --closed=false
taiga story update 246 --status "In progress" --add-tag cli --dry-run
taiga story update 246 --description-file notes.md
taiga story update 247 --add-assignee me --remove-assignee jdoe --block "waiting for the B6 review"
taiga story comment 246 --body-file note.md
taiga story comments 246 --output text
Custom fields
| Command | What it does |
|---|---|
taiga field list --kind story|task |
The project's custom field definitions |
taiga field create --kind story|task --name NAME --type text|date|checkbox [--description TEXT] [--dry-run] |
Create a definition unless one with that name exists |
taiga story field list REF / taiga story field set REF ["Name=value"]... [--unset NAME]... [--dry-run] [--force-version] |
Read and merge a story's values |
field createis idempotent: an existing definition with the same name (case-sensitive) and type is returned unchanged; one with another type, or another description when--descriptionis given, isfield_definition_conflict(exit 2). Definitions are never changed or deleted.- Only
text,dateandcheckboxare supported so far. Values: text as is (nullis text), checkboxtrue/false, dateYYYY-MM-DD. The name ends at the first=. --unset NAME(repeatable, name or id) clears acheckboxordatefield: Taiga storesnullunder its key, which stays even when it is the last field (Taiga refuses an empty dictionary). A field without a value, or alreadynull, writes nothing. Text fields cannot be unset (usage error); set them to empty withName=. JSON output shows the cleared value asnull; text output shows it empty, like a field without value. The same merge, check and--dry-runapply.story field setreads the values, merges the named fields and writes the whole dictionary with theversionof the values resource, not the story's. Unchanged values write nothing. Values of fields without a definition are kept.- Taiga never refuses an old
versionon custom field values, so it cannot stop a concurrent write from being overwritten. The CLI checks the answer instead: if another write landed next to ours, it exits withfield_values_postcondition_failed(exit 4) and the write was applied; check withstory field listinstead of re-running.--force-versionskips the check. See docs/api-notes.md.
taiga field create --kind story --name "Tested in staging" --type checkbox
taiga story field set 246 "Tested in staging=true" "Delivery=2026-10-15" "Notes=a=b is fine"
taiga story field set 246 --unset "Tested in staging" --unset "Delivery"
Project configuration
| Command | What it does |
|---|---|
taiga status list [--kind story|task] |
The project's statuses in board order |
taiga project plan -f FILE|- |
Compare the project with a TOML file; reads only |
taiga project apply -f FILE|- [--dry-run] |
Create the statuses and story fields the file declares and the project lacks |
The file declares story statuses and story custom fields (docs/examples/taiga-project.toml); unknown keys are errors:
[[story_status]]
name = "Waiting for deployment"
color = "#40A8E4" # #RRGGBB, required
closed = false # default false
after = "Ready for test"
[[story_field]]
name = "Tested in staging"
type = "checkbox" # text, date or checkbox
description = "" # default ""
- Only what the file declares is managed and nothing is ever updated or deleted. Statuses and fields missing from the file are listed as
unmanagedand kept. A declared one that exists with another color,closed, type or description — or a name that differs only by case — isdrift:planshows it andapplyrefuses (definition_drift, exit 2) before writing anything. - Names are case-sensitive. Two statuses or fields with the same name in Taiga are refused (
ambiguous_name). - New statuses are created after the last status, in file order.
afterplaces a status behind another one, existing or declared; a cycle, a self-reference or an unknown status is a usage error. Whenafterrequires moving statuses,applywrites the whole order at the end, in onebulk_update_orderrequest. - Reordering is checked, not protected. Taiga 6.7 has no optimistic concurrency for status order (no
version). Right before the write,applyre-reads the order and refuses if anything moved since the plan (project_changed, exit 4, nothing sent); right after, it re-reads and requires the intended order, or exits withstatus_order_postcondition_failed(exit 4): the write was applied and someone else's change landed next to it, so check withtaiga status listinstead of re-running. This detects part of the races, it does not prevent them: a change between the last read and the write can be overwritten. The write is never repeated. applyneedsadmin_project_values(a project admin) and checks it first, also with--dry-run(forbidden, exit 6);planonly reads.--dry-runprints the requests without sending them.applyvalidates everything before the first write, never repeats a write and never undoes one. Its JSON result hasplan,applied,remainingandcomplete;completeistrueonly when a new read finds nothing left. If it stops halfway, the result still goes to stdout with the error on stderr; run it again and it re-plans from Taiga without duplicating anything. If Taiga changed under it, it exits withproject_changed(exit 4). Declarations whose names differ only by case are refused before anything is read; the catalogs are re-read before each creation.
taiga status list --output text
taiga project plan -f taiga-project.toml
taiga project apply -f taiga-project.toml --dry-run
taiga project apply -f taiga-project.toml
Output and exit codes
Without a terminal on stdout, taiga prints JSON; on a terminal it prints text. Force either with --output json or --output text. Errors go to stderr as {"error":{"code","source","stage","cause","recovery"}}; code is stable and cause never contains a secret.
| Exit | Meaning |
|---|---|
| 0 | OK |
| 1 | unexpected error |
| 2 | invalid usage |
| 3 | authentication |
| 4 | version conflict |
| 5 | not found |
| 6 | permission denied |
| 7 | network or server |
Every error code is listed in docs/errors.md.
Authentication
The token for each call is resolved in this order:
TAIGA_TOKEN, used as is, with the type fromTAIGA_TOKEN_TYPE(defaultBearer);- the cached session, while it is valid;
- a refresh of the cached session, under a file lock so concurrent processes refresh once;
- a login with the configured secret:
TAIGA_PASSWORDorTAIGA_PASSWORD_FILE, thensecret_command, the keyring, or the--insecure-storagefile.
TAIGA_TOKEN, TAIGA_PASSWORD and TAIGA_PASSWORD_FILE are only sent to a URL given by --url, TAIGA_URL or the user config. A URL that comes only from a repository's .taiga.toml is refused with auth_untrusted_url, and so is taiga auth login without --url in that case, so a cloned repository cannot redirect your credentials.
| Variable | Use |
|---|---|
TAIGA_URL |
Taiga base URL (https://host; http only for localhost) |
TAIGA_PROJECT |
project slug |
TAIGA_TOKEN |
token used as is |
TAIGA_TOKEN_TYPE |
Authorization scheme for TAIGA_TOKEN (default Bearer) |
TAIGA_USERNAME |
username for the session and for password logins |
TAIGA_PASSWORD |
password for a login without the keyring |
TAIGA_PASSWORD_FILE |
file holding the password |
TAIGA_CONFIG |
config file (default ~/.config/taiga/config.toml) |
TAIGA_STATE_DIR |
state dir holding the session cache (default ~/.local/state/taiga) |
taiga auth login stores the password in the Secret Service keyring. Use --secret-command "pass show taiga" to read it from a password manager instead (the command is split on spaces and runs without a shell), or --insecure-storage to keep it in a 0600 file. taiga auth refresh renews the session, taiga auth logout deletes the local session and secret, and taiga auth status --diagnose tests every source and the environment:
taiga auth status --diagnose --output text
Coding agents and sandboxes
- A person runs
taiga auth loginonce per machine; agents only read the session cache and never see the password. - An agent that gets
session_expiredinside a sandbox cannot renew the session there: runtaiga auth refreshoutside the sandbox and retry.taiganever spends the stored refresh token when it cannot save the new one, and with a read-only cache it logs in only withTAIGA_PASSWORD/TAIGA_PASSWORD_FILE(keeping that token in memory), never with the keyring,secret_commandor the--insecure-storagefile, whether or not a session exists. Without a session and without an env password it fails withsession_cache_readonly: runtaiga auth loginoutside the sandbox. - Codex: enable
network_access = trueand add the state dir towritable_roots:
[sandbox_workspace_write]
network_access = true
writable_roots = ["/home/you/.local/state/taiga"] # absolute path to the state dir
Headless Linux keyring
On a server without a desktop session, run GNOME Keyring for the Secret Service only. The unlock must be what starts the daemon: a daemon that is already running (started by a unit or by D-Bus activation) stays without an unlocked login collection.
# Debian/Ubuntu
sudo apt-get install -y gnome-keyring libsecret-tools dbus-user-session
loginctl enable-linger "$USER" # keep the user manager and its D-Bus running without an open session
# once per boot, before anything uses the keyring: unlock
# (the first unlock creates the "login" collection with this password)
read -rs KP && printf '%s' "$KP" | gnome-keyring-daemon --unlock --components=secrets >/dev/null; unset KP
# check
busctl --user list | grep org.freedesktop.secrets
secret-tool store --label=probe test probe <<<"ok" && secret-tool lookup test probe && secret-tool clear test probe
taiga auth status --diagnose
If something reached the keyring before the unlock, secret-tool fails with Object does not exist at path /org/freedesktop/secrets/collection/login and taiga reports keyring_no_default, keyring_locked or secret_missing: stop that daemon with pkill -x gnome-keyring-d and unlock again. Run these commands in bash, in the user's own login session (for example over SSH), so that DBUS_SESSION_BUS_ADDRESS points to the user bus.
The same keyring serves other tools that use libsecret, such as sonar auth login. KeePassXC and KWallet do not work without a graphical session. If unlocking at every boot is not acceptable, use --secret-command with a password manager.
Development
go test ./...
docker compose -f compose.test.yml up -d && scripts/taiga-seed && go test -tags integration -p 1 ./...
Integration tests only run against the local Taiga from compose.test.yml. Design notes and plans are in docs/superpowers/specs/ and docs/superpowers/plans/; Taiga API behaviour observed so far is in docs/api-notes.md.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
taiga
command
|
|
|
internal
|
|
|
app
Package app resolves the selected project and names into ids and computes minimal writes for the curated commands.
|
Package app resolves the selected project and names into ids and computes minimal writes for the curated commands. |
|
auth
Package auth resolves Taiga credentials: env, session cache, refresh and secret sources.
|
Package auth resolves Taiga credentials: env, session cache, refresh and secret sources. |
|
cli
Package cli wires the taiga command tree.
|
Package cli wires the taiga command tree. |
|
config
Package config resolves file locations, the user config file and the Taiga URL/project context.
|
Package config resolves file locations, the user config file and the Taiga URL/project context. |
|
output
Package output renders results and errors as text or JSON and defines exit codes.
|
Package output renders results and errors as text or JSON and defines exit codes. |
|
taiga
Package taiga is an HTTP client for the Taiga REST API v1.
|
Package taiga is an HTTP client for the Taiga REST API v1. |