Configuration¶
kata configuration is split between environment variables, committed workspace bindings, local per-machine overrides, and daemon config.
Closing-session transcripts¶
To help readers find the chat that completed an issue, enable transcript
attachment in the invoking CLI client's <KATA_HOME>/config.toml:
[close.transcript]
enabled = true
agentsview_url = "https://agentsview.example" # optional; a base path is supported
This is off by default and needs no daemon restart. Attachment needs a daemon
with HTTP API 0.26.0 or newer. Before attaching a transcript, kata close
checks the daemon's API version; an older daemon gets a warning on stderr and
the close proceeds without the transcript. With attachment disabled, close
requests remain unchanged.
When kata close supplies evidence, it attaches a separate transcript object
to the same durable issue.closed event. Evidence and substantive-message
requirements still apply: a chat identifier is provenance, not proof that work
passed. No transcript body, local filename, token, or hook ownership hash is
copied. The setting affects CLI closes, including remote-daemon calls; browser
closes do not infer a session from the daemon's environment.
Claude Code context comes from CLAUDE_CODE_SESSION_ID. Codex context comes
from CODEX_THREAD_ID, falling back to CODEX_SESSION_ID; if both Codex
variables are populated they must agree. A child agent inherits its parent's
variables, so Claude Code and Codex context together is ambiguous and skipped.
For a nested agent or an explicit harness handoff, pass the actual current
native session UUID as a pair:
KATA_TRANSCRIPT_AGENT=claude \
KATA_TRANSCRIPT_SESSION_ID=00000000-0000-4000-8000-000000000001 \
kata close example-project#abc4 --done \
--message "Implemented the requested behavior and ran the focused tests." \
--test "go test ./internal/example"
Only Claude Code and Codex native UUID sessions are supported initially. Kata never scans recent chat files or reuses another issue's session. Missing, invalid, partially supplied, or ambiguous context warns on stderr and skips the attachment; the close can still succeed. An unconfigured AgentsView URL simply stores the identifier. An invalid URL warns and stores only the identifier. URLs must be absolute HTTP(S), with no embedded credentials, query, or fragment.
AgentsView's native session IDs and routes are codex:<UUID> at
/sessions/codex/<UUID> and Claude's bare <UUID> at /sessions/<UUID>. These
routes are supported by the AgentsView v0.44.0 router,
including its PostgreSQL server.
Set agentsview_url only for an installation indexing those native IDs.
Imported archives can namespace IDs (for example, workstation~codex:<UUID>);
for those installations omit the base URL and use AgentsView's Go to
session lookup
with the saved UUID, or supply its verified exact URL through the HTTP close
API. If multiple archives contain that UUID, lookup reports ambiguity; use the
archive's canonical ID or a verified locator. Kata does not infer an import
namespace.
Kata makes no AgentsView request during close. A generated URL is a locator, not an availability receipt: offline hosting, delayed indexing, authentication, or a session excluded from the archive can prevent it from opening. The identifier remains useful for lookup after indexing catches up. Retry the AgentsView lookup, not the close, when indexing lags.
An unkeyed close of an already closed issue adds no event and cannot replace
its provenance. Reopen/reclose records a new session on a new close event and
retains the old event. Keyed retries return their original event; keep the
original session, evidence, actor, and other request fields unchanged. The
AgentsView link is not part of retry identity, so changing agentsview_url
does not break a retry. Retrying from another chat with different automatic
context conflicts; explicitly pass the original agent/session pair.
--dry validates without writing an event or attaching persistent provenance.
Readers can open the session link in the web issue's Events history, or retrieve provenance with the event API. For example, in a small project:
kata --project example-project events --after 0 --limit 100 --json |
jq '.events[] | select(.type == "issue.closed" and .issue_short_id == "abc4") |
.payload.transcript'
For a larger history, continue with next_after_id until the desired event is
found. A saved close receipt also contains event.payload, encoded as a JSON
string; on a keyed retry use original_event.payload. Both contain the same
transcript object as the parsed event stream.
Environment variables¶
| Variable | Meaning |
|---|---|
KATA_HOME |
Data directory. Defaults to ~/.kata. |
KATA_DSN |
Explicit database DSN. Accepts a bare SQLite path, sqlite://..., postgres://..., or postgresql://.... |
KATA_DB |
Legacy explicit SQLite database path. Used when KATA_DSN is unset. |
KATA_POSTGRES_SCHEMA |
Dedicated Postgres schema. Defaults to kata. |
KATA_POSTGRES_SCHEMA_MODE |
Postgres startup policy: bootstrap or validate. Defaults to bootstrap. |
KATA_POSTGRES_SCHEMA_OWNER |
Trusted owner role for the selected schema. Required in validate mode. |
KATA_POSTGRES_ALLOW_INSECURE |
Set to 1 only to permit a non-loopback Postgres connection without server-identity-verified TLS. |
KATA_AUTHOR |
Default actor for mutations. |
KATA_TEAMMATE |
Optional teammate attribution for comments and newly created issues. A command-level --teammate overrides it; --teammate='' suppresses it. |
KATA_TRANSCRIPT_AGENT, KATA_TRANSCRIPT_SESSION_ID |
Explicit current closing session (codex or claude, and its native UUID). Used only when close transcript attachment is enabled. Supply both; they override Claude Code and Codex environment context. |
KATA_INBOX_USER |
Exact actor or actor/teammate inbox address for kata inbox when --for is omitted. It does not set attribution and is independent of KATA_AUTHOR and KATA_TEAMMATE. |
KATA_SERVER |
Remote daemon URL. Skips local discovery and auto-start. |
KATA_AUTH_TOKEN |
Bearer token for daemon API auth. |
KATA_AUTH_TOKEN_FILE |
Overrides [auth].token_file. A nonempty KATA_AUTH_TOKEN continues to win. A selected file failure stops startup and client construction. |
KATA_LISTEN |
Daemon API bind address. Overrides listen in the daemon config; --listen wins over both. |
KATA_WEB_LISTEN |
Overrides [web].listen, the separate browser listener used with a Unix-socket API. |
KATA_WEB_PUBLIC_ORIGIN |
Overrides [web].public_origin, the exact root browser origin. Non-loopback plain HTTP requires private-network trust. |
KATA_SEARCH_EMBEDDINGS_BASE_URL |
Overrides [search.embeddings].base_url. |
KATA_SEARCH_EMBEDDINGS_MODEL |
Overrides [search.embeddings].model. |
KATA_SEARCH_EMBEDDINGS_DIMS |
Overrides [search.embeddings].dims. Must be a non-negative integer; 0 keeps the default dimensionality. |
KATA_SEARCH_EMBEDDINGS_API_KEY_FILE |
Overrides [search.embeddings].api_key_file; the existing credential precedence and file restrictions apply. |
KATA_SEARCH_EMBEDDINGS_DOCUMENT_PREFIX, KATA_SEARCH_EMBEDDINGS_DOCUMENT_SUFFIX, KATA_SEARCH_EMBEDDINGS_QUERY_PREFIX, KATA_SEARCH_EMBEDDINGS_QUERY_SUFFIX |
Override the matching [search.embeddings] affix. Kata keeps the value's leading and trailing spaces; a blank value leaves the file setting in place. |
KATA_SEARCH_EMBEDDINGS_REQUEST_DIMENSIONS |
Overrides [search.embeddings].request_dimensions. Accepts true or false (also 1/0); any other value stops startup. |
KATA_BACKUP_DIR |
Destination for scheduled full JSONL backups; overrides [backup].dir. |
KATA_BACKUP_INTERVAL |
Overrides [backup].interval. Positive duration; defaults to 24h when a backup directory is configured. |
KATA_BACKUP_RETAIN |
Overrides [backup].retain. Positive duration; defaults to 720h. |
KATA_TRUST_PRIVATE_NETWORK |
Set to 1 to permit trusted plaintext bearer use on private non-loopback HTTP. Without it (or allow_insecure), a plaintext non-loopback target fails when the client is built, before any request is sent. |
KATA_ALLOW_UNAUTHENTICATED_PRIVATE_NETWORK_WRITES |
Set to 1 to permit tokenless writes and event streams on a literal private-IP daemon bind. |
KATA_ALLOW_IDENTITY_CONNECTOR_ADMINISTRATION |
Set to 1 to let database-backed identity tokens administer connectors and external-root bridges. Off by default. |
KATA_ALLOW_INSECURE |
Set to 1 or true to allow a configured remote daemon hostname over plain HTTP. Federation uses kata federation enroll --allow-insecure and kata federation join --allow-insecure instead because enrollment credentials are stored separately. |
KATA_TELEMETRY_ENABLED |
Set to 0, false, no, or off to disable anonymous PostHog telemetry. Values are case-insensitive and trimmed. |
KATA_HTTP_TIMEOUT |
Timeout for configured-remote connectivity probes and non-streaming CLI requests, such as 30s or 2m. Defaults to 5s; raise it for bulk imports. It also overrides the federation sync client's separate 60-second request budget. Larger values increase how long an unreachable remote can delay a command or sync attempt. |
KATA_AUTOSTART_IDLE_TIMEOUT |
Overrides autostart_idle_timeout. Empty or 0 disables idle shutdown; positive values must be at least 10s. |
KATA_LINEAR_TOKEN |
Default daemon-owned Linear credential; [linear_sync].token_env selects another variable. |
KATA_TWENTY_TOKEN |
Default daemon-side Twenty API key; [twenty_sync].token_env can select another environment variable. |
KATA_TICKTICK_TOKEN |
Default daemon-side TickTick access token; [ticktick_sync].token_env can select another environment variable. |
KATA_PLANE_TOKEN |
Default daemon-side Plane API key; [plane_sync].token_env can select another environment variable. |
KATA_TODOIST_TOKEN |
Todoist daemon API token; [todoist_sync].token_env selects another variable. |
KATA_NOTION_TOKEN |
Default daemon-side read-only Notion credential; [notion_sync].token_env can name another environment variable. Client workstations need no Notion token. |
KATA_GITHUB_TOKEN |
Default explicit token source for GitHub sync when no matching [[github_sync.app]] credential is configured. It is scoped to github.com unless [github_sync].token_host names a different host. [github_sync].token_env can name a different env var. |
KATA_GITHUB_SYNC_ALLOWED_HOSTS |
Comma-separated exact GitHub Enterprise hostnames trusted for GitHub sync and git-remote inference. github.com is always trusted. |
KATA_FEDERATION_PULL_INTERVAL_MS |
Federation runner poll interval for tests or latency-sensitive private deployments. |
PORT |
Hosted-mode listener port when no explicit listener is configured and the daemon is not an auto-start child. |
XDG_RUNTIME_DIR |
Runtime socket parent on Unix when applicable. |
Database selection¶
kata resolves its database in this order:
KATA_DSNKATA_DB[storage].dsnin<KATA_HOME>/config.toml<KATA_HOME>/kata.db
Bare paths and sqlite:// DSNs select SQLite. postgres:// and
postgresql:// DSNs select Postgres. A standalone Postgres open owns the
dedicated kata schema, prepares it under an advisory lock, and never uses
public for kata tables. KATA_DB stays ahead of [storage].dsn so existing
shells and scripts keep using their explicit database path after the
config-file key is introduced.
For example:
export KATA_DSN='postgres://kata:password@db.example/kata?sslmode=verify-full&sslrootcert=system'
kata daemon start
Production deployments should prepare the schema with a privileged role and run the daemon with a separate DML-only role:
[storage]
dsn = "postgres://kata_runtime@db.example/kata?sslmode=verify-full&sslrootcert=system"
[storage.postgres]
schema = "kata"
mode = "validate"
schema_owner = "kata_schema_owner"
# Dangerous lab-only escape hatch for remote plaintext or unverified TLS:
# allow_insecure = true
bootstrap creates or advances the configured schema and therefore requires
DDL authority. validate performs no DDL and requires both schema_owner and
the exact schema version for the running binary. Environment values override
the [storage.postgres] keys. See PostgreSQL operations
for the split-role installation and upgrade ceremony.
Every non-loopback, non-Unix Postgres connection candidate must use TLS with
server identity verification. This includes fallback hosts generated by the
DSN. sslmode=disable, allow, prefer, require, and verify-ca do not meet
that remote requirement by themselves. Use sslmode=verify-full with system
roots or an explicit CA. The allow_insecure setting and
KATA_POSTGRES_ALLOW_INSECURE=1 are deliberate lab-only exceptions; they apply
to the whole Postgres connection configuration and can expose database
credentials and all stored data to a network-positioned attacker.
KATA_DSN and [storage].dsn are shape-validated before use. Unknown schemes
are rejected, and common Postgres-only query parameters on a bare path or
sqlite:// DSN are treated as likely formatting mistakes. Validation is local:
it does not dial Postgres or stat SQLite paths.
Workspace binding¶
.kata.toml is committed with the project:
It should stay secret-free.
Local override¶
.kata.local.toml is gitignored. Use it for machine-specific daemon routing:
KATA_SERVER wins over the local file unless a command passes
--daemon <name>.
Daemon target resolution order is:
--daemon <name>KATA_SERVER.kata.local.toml[server].urlor[server].daemonactive_daemonin<KATA_HOME>/config.toml- local daemon discovery or auto-start
To pin an existing home on this machine, use [server].daemon = "work"
instead of url, and register its identity in the client home's catalog:
[[daemon]]
name = "work"
local = true
home = "/absolute/work-home"
instance_uid = "01J00000000000000000000001"
Use the existing home's observed UID, not the example UID. home and
instance_uid are paired, local-only fields; home is absolute or starts with
~/. A workspace server.daemon accepts only such a profile and cannot coexist
with server.url. Profile startup reads the selected home's config and checks
its existing database identity and schema before starting a process. Legacy
local = true without home retains current-home behavior. See
Local daemon profiles for registration,
isolated credentials, PostgreSQL restrictions, and recovery.
Committed .kata.toml files bind the project name only; do not put daemon
routing or tokens there.
For trusted private-network hostnames that cannot be represented as literal non-public IP addresses, opt in per target:
Client display preferences¶
[display] belongs to the client reading <KATA_HOME>/config.toml. It is not
sent to a remote daemon and does not change daemon rendering or API responses.
kata show --render uses the built-in terminal Markdown renderer by default.
To use an external stdin/stdout renderer instead:
The value is an argv array. kata passes it verbatim without a shell, appended flags, width injection, or environment changes. For example:
kata starts the configured program once per non-empty Markdown field. Each
invocation has a 10-second timeout. After it expires, kata spends up to 2
seconds on platform-specific termination: Unix signals and then force-kills the
renderer process group, while Windows waits through the grace period and then
force-kills only the renderer process. kata then bounds any remaining process
or captured-pipe wait by a separate 2-second interval. A timed-out call can
therefore take about 14 seconds to return. Because the child's stdout is
captured, users are responsible for renderer-specific color and width flags or
inherited environment variables such as CLICOLOR_FORCE. After capture, kata
normalizes the output and ANSI-safely hard-wraps it to the terminal width for a
description or the remaining field width for a comment. kata treats output
with or without a final newline the same, while preserving internal blank
lines. Renderer stderr is discarded because a program may echo the Markdown
input there; run the configured argv directly to diagnose renderer-specific
failures.
The common daemon-config path recognizes [display] without decoding or
semantically validating it, so unknown display keys and invalid display values
do not break daemon startup. A TOML syntax error anywhere in config.toml still
prevents common config parsing. kata validates this client section only when
show --render is active on a terminal, so display-only semantic mistakes do
not break plain output or redirected output.
TUI preferences¶
Configure the terminal UI in <KATA_HOME>/config.toml:
With confirm_quit = false, pressing q quits immediately when no input or
dialog is open. Omit the setting or set it to true to keep the default quit
confirmation. Ctrl+C remains an immediate quit shortcut in those views.
This is a client preference; it does not change the daemon or other clients.
Daemon config¶
<KATA_HOME>/config.toml can configure storage, listener, auth behavior, and
named daemon targets:
listen = "100.64.0.5:7777"
active_daemon = "shared"
timezone = "America/Los_Angeles"
[[daemon]]
name = "shared"
url = "http://100.64.0.5:7777"
token_env = "KATA_SHARED_TOKEN"
[storage]
dsn = "/var/lib/kata/kata.db"
[auth]
token = "change-me"
trust_private_network = true
[web]
listen = "127.0.0.1:27777"
public_origin = "https://daemon.example"
[twenty_sync]
api_origin = "https://api.twenty.com"
web_origin = "https://app.twenty.com"
token_env = "KATA_TWENTY_TOKEN"
[ticktick_sync]
token_env = "KATA_TICKTICK_TOKEN"
[plane_sync]
api_origin = "https://api.plane.so"
web_origin = "https://app.plane.so"
token_env = "KATA_PLANE_TOKEN"
[notion_sync]
token_env = "KATA_NOTION_TOKEN"
[github_sync]
token_env = "KATA_GITHUB_TOKEN"
token_host = "github.com"
[[github_sync.app]]
host = "github.com"
owner = "example-org"
app_id = 12345
installation_id = 67890
private_key_path = "/var/lib/kata/github-app.pem"
Idle shutdown is intended for the default owner-local daemon rather than the shared-daemon configuration above. Its minimal configuration is:
The listener precedence is --listen, KATA_LISTEN, top-level listen,
hosted-mode PORT, then the platform default. Environment overrides are trimmed;
empty or whitespace-only values leave the corresponding config-file value unchanged.
Plain kata daemon start starts the daemon in the background and returns after
startup is confirmed; use kata daemon start --foreground for service-manager
and hosted deployments. Auto-started daemons also read the config-file listener
value.
An empty [storage].dsn means "no storage override"; env vars or the default
database path still apply.
Daemon bearer credentials use [auth].token > [auth].token_file >
[auth].token_env (the name of a variable containing the token). A nonempty
KATA_AUTH_TOKEN overrides the inline token. Token files follow the
embedding credential file restrictions: absolute or ~/
paths, regular files owned by the daemon user, no Unix group/world permissions
or Windows access for other users, and at most 64 KiB. Whitespace and a
trailing newline are trimmed. An unusable
selected file or named environment variable fails loudly without falling back.
For example, mount a 0600 secret file owned by the daemon user and set
KATA_AUTH_TOKEN_FILE=/run/secrets/daemon-token.
autostart_idle_timeout lets an implicitly started, owner-local daemon exit
after a period without client activity. It is off by default. The setting is
ignored for explicit daemon starts and for daemons exposed through non-loopback
listeners, a public web origin, trusted-proxy configuration, shared-listener
host aliases, or an enabled [federation.ingress] listener. kata daemon start replaces a running idle-eligible auto-started
daemon with an explicit one so it stays resident, and kata daemon restart
always starts an explicit daemon. The daemon writes one
kata daemon: idle shutdown after ... line to its log when it exits for this
reason. Active requests and already-admitted finite background work
receive a bounded drain before process exit, so exit can occur after the
configured interval. Ordinary health probes do not renew the timeout. A running
kata mcp serve process discovers the effective timeout from /health and
sends marked GET /api/v1/ping keepalives after applicable listener policy
checks in both stdio and streamable-HTTP modes so the bridge remains usable for
its full lifetime. Use an explicit daemon service when
GitHub/Notion/Plane/Linear/Twenty/TickTick sync, federation, or timed-claim maintenance must remain continuously
scheduled without a client present.
The optional top-level timezone is the IANA timezone for date-only and local
date-time scheduled_on values that do not have an issue-level timezone. If
both are unset, Kata uses UTC. RFC 3339 scheduled_on values ending in Z are
UTC instants and do not use this setting.
The web UI's daemon selector lists URL entries and home-less local entries.
Home-bearing profiles are omitted and cannot alias the serving daemon. A plain
kata ui opens a selected local profile directly; otherwise it starts or
discovers the local browser gateway and initially selects active_daemon; changing the selection keeps configured tokens on the daemon
side. Use kata ui --daemon <name> only when opening one named target directly
is preferred. Identity-authenticated tabs can read remote gateway targets, but
must open the target directly for writes because the gateway does not delegate
browser identities. Request-actor tabs remain writable only when the target
advertises the same request-actor policy; the gateway rechecks that target's
authenticated capabilities immediately before every mutation.
[web].listen selects the browser listener when the normal daemon transport
cannot also serve HTTP. If omitted, Kata binds 127.0.0.1:0, lets the operating
system assign an available port, and publishes the resolved URL through
kata daemon status. Configuring [web].listen with port 0 has the same
behavior; set a nonzero port when a fixed browser origin is required. The
top-level listen remains the daemon API listener and is shared with the
browser when it is TCP (including Windows and hosted mode).
On a direct-loopback origin, a fresh browser tab transparently receives a
local-web session. A static daemon token does not disable this owner-local path.
Kata disables it when the listener or public origin is non-loopback, a forwarding
header is present, or identity or trusted-proxy authentication is configured.
Authenticated browser requests still require both the HttpOnly cookie and
tab-local session header.
If the browser listener is itself named in
[auth.proxy].trusted_proxy_listeners, the browser transparently exchanges the
proxy-asserted actor for that tab-scoped session instead of showing token login.
[web].public_origin declares the exact HTTP or HTTPS origin visible to the
browser when a same-origin TLS terminator or development proxy sits in front of
Kata. It must contain only scheme and authority: no credentials, path, query,
or fragment. Kata never derives this security boundary from forwarded request
headers. The proxy must preserve streaming for /api/v1/events/stream and
route the SPA, assets, session endpoints, and data API to the same daemon.
[web].allowed_hosts is an exact allowlist of additional HTTP Host
authorities accepted by a shared TCP listener. Use it when a daemon bound to a
wildcard address is intentionally reached through a container alias or another
backend-only name that is neither the bind authority nor public_origin:
Entries contain only host or host:port, with no scheme, path, credentials,
query, or fragment. KATA_WEB_ALLOWED_HOSTS supplies a comma-separated
environment override for ephemeral deployments. Unlisted Host values are
rejected before API or browser route handling, even when a request presents a
bearer header; this prevents credentials from turning DNS rebinding into an
authority bypass.
[notion_sync].token_env names the daemon environment variable containing its
Notion read-content credential (default KATA_NOTION_TOKEN). Share each task
database with the Notion internal connection. The credential is shared across
Notion bindings, resolved once per run, and sent only to fixed
https://api.notion.com:443; redirects are rejected. Supply it in the daemon's
service environment, including for remote daemons, and restart after service
environment/config changes. Tokens and environment-variable selectors never
enter binding config or JSONL. Source/mapping IDs, interval, and cutoff are
project binding settings managed by kata sync notion enable, not keys in
[notion_sync]. See Notion sync.
[github_sync] controls daemon-side GitHub credentials. The recommended shared
daemon path is [[github_sync.app]], matched exactly by normalized (host,
owner). The GitHub App needs only Metadata read and Issues read permissions.
If no App matches a binding, kata reads the environment variable named by
[github_sync].token_env (default KATA_GITHUB_TOKEN) only when the binding
host matches [github_sync].token_host (default github.com). If no host-bound
env token matches, kata falls back to gh auth token --hostname <host> for
local/single-user deployments. GitHub Enterprise hosts still must be listed in
KATA_GITHUB_SYNC_ALLOWED_HOSTS, and Enterprise env-token deployments should
set both token_env and token_host.
For a single-user private network where the private IP itself is the access
boundary, omit token and use:
This permits writes and event streams without bearer auth, with client-supplied
actor attribution. It requires a literal private-IP bind and cannot be combined
with token, require_token_identity, or --insecure-readonly; token
administration endpoints remain blocked.
Postgres DSNs may carry credentials. Runtime redaction strips userinfo and query parameters before a DSN appears in daemon metadata, health output, import output, errors, or per-database namespace hashing. Use environment variables or secret-managed configuration rather than committing a credential-bearing DSN.
Connector instances¶
Each [[connector]] table configures one external-root connector process for
issue bridges:
[[connector]]
id = "notes"
command = "/usr/local/bin/kata-connector-notes"
args = ["--workspace-mode"]
timeout_seconds = 30
[connector.env]
NOTES_TOKEN = "KATA_NOTES_TOKEN"
[connector.settings]
workspace = "example-workspace"
id is the lowercase instance name used by kata connector and
kata bridge --connector; it must be unique. command is the absolute path of
the connector executable and args its fixed arguments. timeout_seconds
bounds each connector call (omit it for the default). connector.env maps
child environment variable names to daemon environment variable names; only
these variables reach the connector process, and their values are redacted from
connector errors as secrets. connector.settings is non-secret JSON-compatible
configuration passed to the connector on every call. The daemon validates this
structure at startup and refuses to start on an invalid [[connector]] table,
but it does not run the executable or read the mapped environment sources
until a connector call needs them; connector health is observed through
kata connector status. The bridge reconciliation workers always run, so
durable bindings are still handled when their connector is removed from
configuration.
Federation request signing and ingress¶
[federation.signing] configures hub-controlled required, the fixed HTTPS
external_url (including a prefix), replay_state_file, and [[federation.signing.key]]
entries. Each entry binds key_id, enrollment_id and exactly one of key_env
or key_file; an optional Unix not_after bounds retirement overlap.
[federation.ingress] enables an independent allowlisted listener with enabled
and a literal private listen address. It requires signing configuration and
valid signatures regardless of required on ordinary listeners. It defaults to
disabled, or 127.0.0.1:0 when enabled without an address. See
Federation request signing for key setup,
replay initialization, topology, limits and recovery.
Declarative federation mappings¶
A spoke daemon maps local projects to projects on remote daemon-catalog targets. Each mapping uses either catalog administration credentials or a local credential provider. It cannot mix them.
Catalog administration credentials¶
This form creates projects and enrolls or adopts direct-spoke replicas at
startup. It requires the hub owner's administration credential outside identity
mode. To connect with an ordinary account token, configure a catalog entry and use
kata federation bridge connect; do not add a
[[federation.project]] mapping for that bridge.
[[daemon]]
name = "team-hub"
url = "https://hub.example"
token_env = "KATA_TEAM_HUB_TOKEN"
[[federation.project]]
hub = "team-hub"
spoke_project = "spoke-project"
hub_project = "hub-project"
actor = "user-a"
hub names a remote [[daemon]] entry; its URL, authentication, and
allow_insecure policy are reused without falling back to the spoke daemon's
global bearer token. Prefer token_env so the hub administration credential
does not appear in config.toml. An unset or empty selected token_env is a
runtime authentication failure: the spoke stays available and retries without
sending another credential.
Each mapping ensures the named hub project and a pull,push,lease enrollment,
then creates the local spoke_project if it is missing or adopts the existing
standalone project. Enrollment credentials are generated automatically and
stored in the spoke's owner-only federation credential store. A generated
credential is durably reserved once under the resolved hub project UID before
enrollment. A database-backed account token is refused on this legacy
enrollment path with federation_enrollment_requires_relay.
Credential-file updates use a same-directory, failure-atomic replacement so a
failed write cannot truncate the last readable credential set.
If the named hub project is deleted and recreated, its UID changes. kata
reports a conflict and does not silently enroll the replacement. The category
is configuration_conflict before adoption and binding_conflict after the
local project is bound.
Run kata federation leave <spoke-project> to clear the old managed
reservation, verify the mapping, and restart the daemon to enroll again.
Mappings are loaded once when the daemon starts. Restart the daemon after
adding or changing one. Reconciliation runs asynchronously: hub outages,
authentication failures, and runtime conflicts do not delay daemon readiness
or make /health unhealthy. Each mapping retries independently with
exponential backoff from one second to a five-minute cap.
Changing a named catalog entry's URL does not silently rewrite existing spoke
bindings. For a config-managed spoke, reconciliation reports
binding_conflict and changes nothing during the catalog-edited-but-not-yet-
rebound window, including after a restart. Resolve that expected migration
state explicitly:
The selected spoke daemon resolves team-hub from its own startup config. It
requires HTTPS, validates the existing enrollment against the same hub project
ID and UID at the new endpoint, then updates the stored endpoint without
changing the enrollment token, capabilities, actor, project identity, or sync
cursors. The catalog entry's administration token or token_env is not used
for this validation. Restart first if the edited catalog has not yet been
loaded by the daemon.
Before changing either local endpoint record, rebind drains in-flight federation transport for that project and blocks new transport until the credential and binding agree. A queued sync then rereads the new endpoint.
Removing a mapping and restarting stops managing it; it does not detach the existing replica or revoke its hub enrollment. Teardown is always explicit:
Explicit leave also removes exact config-managed credential reservations left by an interrupted startup reconciliation, including reservations created before local adoption completed. Conflicting or manual credentials are retained and reported as cleanup errors instead of being deleted. If leave removes a reservation while hub enrollment or rotation is in flight, reconciliation compensates by revoking the completed enrollment rather than stranding it. Before contacting the hub, leave durably marks the reservation and drains any earlier reconciliation request. A completed enrollment ID remains recorded until local teardown finishes, so a retry or daemon restart can repeat the idempotent revoke. If a crash happens before that ID is recorded, reconciliation replays the reserved token to recover the exact enrollment and then revokes it. After leave completes, the mapping stays suppressed for the lifetime of that daemon process; restart when you deliberately want the configured mapping to enroll again.
Structural mistakes still fail config loading, including missing fields, a hub
that is not a remote catalog entry, duplicate spoke_project values, or two
mappings that select the same canonical hub origin and hub project. actor is
required and cannot be the reserved bootstrap identity, even when a
DB-backed token identity will override the requested actor at reconciliation
time.
Credential provider¶
Use this form when the hub supplies a helper to approve access without giving Kata its administration credentials:
[[daemon]]
name = "team-hub"
url = "https://hub.example/tasks"
[[federation.project]]
hub = "team-hub"
spoke_project = "spoke-project"
hub_project = "hub-project"
intent = "collaborate"
credential_provider = ["access-helper", "credentials", "--profile", "work"]
| Field | Rule |
|---|---|
hub |
Names a remote catalog entry with an HTTPS URL. |
spoke_project, hub_project |
Required local project name and destination project key. |
intent |
Required: read_only, collaborate, or migrate. |
credential_provider |
Nonempty executable/argument array. Kata executes it directly, without a shell. |
actor |
Must be absent; the provider supplies the approved actor. |
Catalog token, token_env |
Must be absent for a provider mapping. |
Provider mappings require HTTPS even if the catalog sets allow_insecure.
The helper approves a credential for an exact HTTPS destination; the catalog's
plaintext opt-in does not change that contract. Install the hub's CA in the
daemon's trust store when using a private certificate authority.
read_only permits pull only. collaborate permits pull and push; the hub may
also grant task claiming. Both require an empty local project. migrate asks
the hub to approve adoption of an existing project's tasks and authors.
The helper cannot silently downgrade write access or upgrade read-only access.
Kata retains the request across restarts and failed exchanges. A helper that
exits 2 reports configuration_conflict in reconciliation health; check its
command and protocol version. Removing a provider mapping releases its saved
request and detaches the replica on restart, unlike catalog-admin mappings.
See External credential providers
for approval, status, and cleanup, and the
helper protocol
for the exact exchange.
Token identity mode¶
For a shared daemon where each user should have stable attribution:
Restart the daemon with this configuration, then create per-user tokens with the bootstrap token:
export KATA_AUTH_TOKEN=bootstrap-admin-token
kata tokens create --actor member --name laptop
kata tokens list
kata tokens revoke 1
Unscoped tokens create prints plaintext once. The daemon stores only a SHA-256 hash.
Lost tokens must be revoked and recreated. Set --expires-in 4h to bound an
ordinary token's lifetime; omitting it keeps the existing non-expiring lifetime.
Kata refuses tokens create when a shared token is configured but
require_token_identity is false. This prevents minting a credential that the
daemon would immediately reject as a bearer token. A local no-auth daemon may
still pre-provision unscoped tokens before it is restarted in identity mode.
Scoped creation always requires require_token_identity = true, even without
a shared token. allow_unauthenticated_private_network_writes is incompatible
with identity mode and refuses token administration, so it cannot mint scoped
credentials. See token CLI usage.
In identity mode, the bootstrap/admin token can manage tokens and perform reads, but attributed writes require a DB-backed token. The daemon derives the actor from that token.
Connector administration is off for DB-backed tokens by default. To grant it,
add this setting to the same [auth] table:
Every active DB-backed token can then use all connector and external-root
bridge routes across the daemon. For attributed mutations, the token actor
overrides any actor in the request body.
This setting changes only DB-backed token authority. The identity-mode bootstrap token, browser sessions, trusted-proxy principals, insecure read-only requests, and unauthenticated private-network requests still cannot administer connectors.
Close throttle¶
kata refuses structurally dangerous close patterns. The parent-completeness guard always refuses closing an issue while it has open children. Normal CLI and API close paths also require close evidence and a substantive message.
By default, kata does not throttle sibling close bursts. Operators who want stricter pacing can enable two additional guards daemon-wide:
- sibling-burst: closing more than three sibling issues within the configured window is refused;
- repeated-message: closing a second sibling with an identical
doneoraudit-no-changemessage within thirty minutes is refused.
Enable the optional throttles with:
enabled defaults to false. window controls only the sibling-burst lookback
and defaults to "60s"; use Go duration syntax such as "30s", "2m", or
"1h". When a sibling-burst close is refused, the error message reports the
resolved window.
Normal CLI and API close paths still run the parent-completeness refusal,
message-substance checks, and evidence checks. The TUI close path skips the
message-substance and evidence checks only when the daemon accepts the request
over an owner-local Unix socket or direct loopback TCP connection with no
forwarding headers, because an interactive human confirms each close. Users of
a forwarded or non-loopback TUI, including a TUI authenticated with an
issue-scoped credential, get a completion form for the substantive message and
typed evidence required by the normal close contract. The TUI discovers that
requirement from /instance; it does not trust its own source=tui marker to
widen authority. Structural guards still apply to every transport.
Semantic search¶
This section is the field reference; see the Semantic search guide for setup and behavior.
Semantic (vector) search is opt-in. With no [search.embeddings] section,
kata search behaves exactly as before (lexical FTS only) and the daemon
makes no embedding network calls. Adding the section enables hybrid search: the
daemon embeds each issue's title and body through an OpenAI-compatible
/embeddings endpoint and fuses vector results with the lexical leg.
[search.embeddings]
base_url = "http://localhost:11434/v1" # any OpenAI-compatible /embeddings
model = "nomic-embed-text"
# api_key = "..." # only for providers that require a key
# api_key_file = "~/.config/kata/embedding.key"
# api_key_env = "VOYAGE_API_KEY"
# fingerprint_salt = "" # bump to force re-embed when model weights change
# document_prefix = "" # literal affix applied to each indexed chunk
# document_suffix = ""
# query_prefix = "" # literal affix applied to each search query
# query_suffix = ""
# request_dimensions = false # send dims only if the provider supports it
# dims # expected vector dimensionality (default 768)
# batch_size # inputs per request (default 64)
# model_context_tokens # model's maximum tokens for one input
# max_batch_tokens # provider's aggregate input-token cap per request
# timeout_seconds # per-request timeout (default 30)
# trust_private_network = false # allow plaintext HTTP to literal non-public IPs
base_url and model are both required once the section exists; setting only
one is a startup error rather than a silent disable. With no credential source
configured, requests omit the Authorization header. Keyless local providers
such as Ollama need only base_url and model.
Affixes preserve every space and newline. Kata applies document affixes once
around each prepared issue chunk and query affixes once around the search
query text, which Kata sends as entered. With empty affixes and
request_dimensions = false, existing generation fingerprints stay unchanged
and stored vectors are reused. Setting any affix or enabling dimension requests
creates a separate generation. dims always validates response width;
request_dimensions additionally sends that width as the request's
dimensions field. Kata never slices vectors to fit.
Optional EmbeddingGemma 2 text endpoint¶
For a conforming OpenAI-compatible text server, this optional configuration uses the EmbeddingGemma 2 model card retrieval recipe and native 768 dimensions:
[search.embeddings]
base_url = "http://127.0.0.1:8080/v1"
model = "embeddinggemma-2-text-r914f7f8-bf16" # example server alias
fingerprint_salt = "914f7f89142e33e77833254d9c9b90c3cef7303b"
dims = 768
document_prefix = "title: none | text: "
query_prefix = "task: search result | query: "
The operator must bind that alias to google/embeddinggemma-2 at the specified
revision, tokenizer, pooling, precision and dimension policy. The alias and
salt record that assertion; Kata does not verify the server's weights. Use
mean pooling including prompts, L2 normalization, and bfloat16 or float32
activations, rather than float16. Configure the server to accept literal client
prompts without adding them again. Kata sends text only; this transport does
not enable image, audio or video input.
The model card recommends title: {title} when a title exists. Affixes are
fixed text, so every chunk uses title: none and the issue title stays at the
start of the first chunk's text.
The server must enforce the shared 8192-token input window, including prompt
overhead. Kata's model_context_tokens and max_batch_tokens only bound batch
packing; they do not tokenize or enforce admission for an individual input.
Character-based chunk limits also do not establish that token bound.
Kata does not download weights or configure the serving runtime.
For 512, 256 or 128 dimensions, set dims to that width and
request_dimensions = true only if the server supports explicit truncation
followed by L2 renormalization. Both queries and documents must use the same
width. A server that returns the wrong width is rejected. Changing these
settings requires restart and backfill before semantic search can resume.
Credentials and batching¶
Credential resolution uses api_key > api_key_file > api_key_env. The first
selected source wins. If its file is missing, unreadable, or empty, or its
environment variable is unset or empty, Kata makes no provider requests and
keeps issues in the backlog. It reports the source problem without falling
back to a lower-priority source.
Key files must use an absolute path or start with ~/, which expands to the
daemon user's home directory (not KATA_HOME). Other relative paths are
rejected. Symlinks are accepted, and there is no parent-directory ownership or
permission policy. Kata validates the opened file: it must be regular and
owned by the daemon's effective user. On Unix, group/world permissions are
rejected (chmod 600). On Windows, the file ACL must restrict access to the
current user, SYSTEM, and Administrators. Files larger than 64 KiB are refused,
and reads are bounded to enforce that limit. File contents are trimmed,
including a trailing newline.
Kata resolves the selected credential once at startup and once per
kata daemon reload. Reload wakes the embedding backlog; changes to provider
settings, including base_url or model, require a restart. An environment key
must be present in the daemon's environment, not just the shell running a later
CLI command.
Missing credentials and provider access failures appear in health and search. Health warns on stderr in human and agent formats while JSON stays clean. Default search labels its lexical fallback without an extra stderr warning; explicit semantic/hybrid searches return readable validation errors. See Missing or rejected credentials for status meanings and recovery.
The embedding API key is attached only to requests whose origin matches
base_url, following the same bearer-token trust ladder as daemon catalog
tokens: HTTPS is always allowed, HTTP to loopback is allowed, and HTTP to other
private IPs needs trust_private_network = true.
Some providers cap the total input tokens across one embedding request as well
as the number of inputs. Set model_context_tokens to the model's per-input
context limit and max_batch_tokens to the provider's aggregate request limit
when that applies. Kata passes both limits to kit, which conservatively treats
every input as capable of filling the model context and reduces batch_size as
needed. This avoids repeatable oversized-request errors from providers that
truncate each input before enforcing their aggregate limit.
Both settings are optional and default to zero. Existing deployments therefore
keep count-only batching. When either setting is used, both must be positive and
max_batch_tokens must fit at least one model_context_tokens input; kit checks
this during daemon startup before Kata processes documents.
Privacy: configuring an endpoint sends issue titles and bodies to it on every embed. That is the consent boundary: the operator who writes this section authorizes the data flow. For sensitive projects, prefer a local endpoint (for example Ollama on loopback) so issue text never leaves the host. Embeddings are local derived state and do not federate: each daemon embeds only what it stores, and no vectors are sent to or pulled from federated hubs.
Index freshness¶
The daemon keeps the index fresh on its own: a background reconciler embeds new
and edited issues within seconds, and kata reports its state under
embeddings in the /health response (configured, last_success_at,
last_error_status, embedded, skipped, and backlog). During a backfill it also
reports started_at and last_progress_at, then adds a smoothed
rate_per_second and eta_seconds after two positive progress samples. Search
never blocks on embedding lag: an issue is findable lexically the instant it
is created, and gains semantic recall once the reconciler catches up.
Issue text is chunked before embedding rather than embedded as a single truncated vector, so long issues get full semantic coverage instead of losing everything past a fixed length cutoff.
With SQLite, embeddings live in a sidecar database the daemon creates next to
the main database (kata.vectors.db for the default kata.db). With
PostgreSQL, they live in halfvec tables in the selected Kata schema when the
optional pgvector extension is installed. See
PostgreSQL operations for extension and role
requirements. Core PostgreSQL storage works without pgvector. Both forms are
derived state and are rebuilt by re-embedding;
portable JSONL exports do not include vectors.
Upgrading to a kata version that changes embedding storage re-embeds every
issue from scratch on the first daemon start after the upgrade. The rebuilt
index starts serving immediately, so search returns partial semantic results
while the backfill drains; the embeddings backlog in /health reports the
remaining coverage. An ordinary reconciler backlog with an active index does
not degrade search; fresh or edited issues simply lack semantic recall
until they are embedded. Search degrades (labeled in auto mode, 503 for
explicit --hybrid/--semantic) when the vector leg is unavailable or when
bounded label filtering exhausts its candidate ceiling before filling the
requested result limit. Unavailability includes the period before any index is
activated (fresh vector storage before the first reconcile cycle) and embedding
configuration changes while the replacement index is still backfilling.
Changing model, dims, fingerprint_salt, any literal affix, or
request_dimensions builds a new index generation
in the background and cuts over automatically once it finishes filling.
During that backfill the vector leg is unavailable (queries embedded under
the new configuration cannot be scored against the old generation's vectors), so
auto searches degrade to labeled lexical results and explicit
--hybrid/--semantic requests return 503 until the cutover.
Telemetry¶
kata sends limited anonymous telemetry to PostHog when the daemon starts, and
then emits an in-process daemon_active heartbeat once per UTC day while the
daemon keeps running. Restarting the daemon may send another heartbeat; kata
does not store heartbeat state in the database.
The events are daemon_started and daemon_active with project_count,
application=kata, build version, commit, OS/arch, source, and the database's
stable anonymous instance_uid as the distinct ID. They do not send project
names, issue refs, issue content, comments, labels, paths, or actor names. GeoIP
collection is disabled and PostHog person-profile processing is explicitly
turned off. Use distinct daemon_active counts for active-install reporting;
daemon_started is only for startup-volume diagnostics.
A new database records when it created its instance_uid in the meta key
instance_created_at. Events from a database with this time carry
install_age_hours, the whole hours since that time, so reports can tell new
installs from established ones. Databases created before kata recorded this
time send events without it.
The web UI reports an anonymous app_opened event to the daemon serving it
when the page loads, and again on the first window focus of each later UTC day.
Background refreshes of an open tab do not count. The daemon sends the event to
PostHog with the same fields, distinct ID, and opt-out as its own events; the
browser never contacts PostHog. Only signed-in sessions with write access on the
serving daemon are counted.
Closing a tab, leaving it hidden for 30 minutes, or exiting kata tui reports
session_ended with surface and duration_bucket: under_1m, 1_to_5m,
5_to_30m, or over_30m. A browser visit adds up visible time across tab
switches and excludes hidden time. Terminal exit reporting uses the daemon that
recorded the opening, including after switching the displayed daemon. Delivery
is best effort and waits at most one second; a rejected authentication request
drops the event.
app_opened carries one property, surface, set to web, tui or cli; the
daemon drops any other value. kata tui reports app_opened to the daemon it
connects to, after connecting. Switching daemons inside the TUI sends nothing
more.
CLI commands such as kata create or kata list that a person runs in a
terminal report app_opened with surface: cli to the daemon they used,
after the command succeeds. Each KATA_HOME reports at most once per UTC day
to each daemon: once the daemon accepts the report, kata records the day under
<KATA_HOME>/telemetry/ and later commands that day send nothing. A report
that fails is retried by the next command. The report never starts a daemon,
waits at most one second and prints nothing.
Only a command whose standard output is a terminal counts, because agents and
scripts read kata's output through a pipe. Probes and local commands such as
kata health or kata version send nothing. Agent output mode (--agent,
--format agent) and kata mcp serve startup send nothing either. An agent that
runs commands in a terminal without --agent counts like a person.
Each client's event goes through the daemon it used, so that daemon's install
ID and its KATA_TELEMETRY_ENABLED setting apply, and a TUI or CLI pointed at
a shared daemon counts that daemon's install. The daemon forwards one
app_opened per surface per UTC day and forgets what it sent when it restarts,
so a restart can send another the same day. Delivery is best effort and never
shows in the terminal; count distinct installs rather than events.
Each admitted MCP tool call, including section loaders and tool errors, reports
agent_active to its daemon. Initialization and tool listing send nothing.
Successful contract hooks report the same event. Attention hooks and daemon hook
children report only when their operation resolves a daemon target.
Human-triggered hook children count as hook execution.
Hook installation, status, and version/help probes without a resolved daemon target
send nothing. Generic hook children report only to the daemon their command used;
contract hooks may discover an existing daemon without starting one.
Ordinary piped commands send nothing.
MCP reporting uses one worker with a queue of 128 observations. Tool calls enqueue without waiting; overflow and queued work at shutdown are discarded. Each report has a separate one-second deadline. Shutdown cancels and joins the worker. CLI hook reports wait at most 100 ms after successful execution; slow remote transports may drop observations. Delivery is best effort and preserves command output, results, and exit status.
The daemon counts these observations for each install and UTC day. It sends one
agent_active with call_count_bucket: 1-10, then agent_call_count at calls 11
and 101 with 11-100 and over-100. Later calls in the same bucket send nothing.
Use distinct agent_active installs for daily hook and MCP activity, separately
from human app_opened counts. The highest bucket across both agent event names
gives the total observed daily volume.
The daemon keeps the day, count capped at 101, and last queued threshold in a
private instance-specific file under <KATA_HOME>/telemetry/, so ordinary restarts
retain the count. Counts cover one serving daemon per install; daemons sharing a
PostgreSQL database do not combine their local counters. A new UTC day resets the
count on the next observation. Failed captures retry on the next observation.
A delayed first capture still sends agent_active with 1-10, followed by the
currently reached companion bucket. Reports share the daemon's install ID and
opt-out and send buckets rather than exact counts, paths, tool names, or actor
names.
The browser and TUI also report screen_viewed when a screen opens. The fixed
names are inbox, today, delegated, scheduled, issues, logbook,
issue, graph, credentials, projects, daemons, federation, help,
and empty. Unknown names are dropped. surface is web or tui, whichever
reaches that screen first that UTC day. One claim per screen in the database
shares the daily count across interfaces, hosts, and daemon restarts. These meta
rows appear in kata export output. A changed
installation ID starts a fresh count. Later-day browser focus or terminal input
counts another visit. Delivery is best effort; a rejected enqueue can retry.
Disable telemetry with:
false, no, and off also disable telemetry, ignoring surrounding
whitespace and letter case. The global TELEMETRY_ENABLED switch and
kit_posthog_disabled build tag retain their existing behavior.
Release archives and package-manager builds use this same telemetry policy.
Installing through Homebrew, a .deb, an .rpm, or another package manager
does not implicitly opt out; set KATA_TELEMETRY_ENABLED=0 before the daemon
starts to disable the events.
Federation credentials¶
Federation enrollment tokens are separate from daemon API tokens. The hub stores only token hashes. A spoke stores the plaintext enrollment token in its local federation credentials file so it can call hub federation transport routes.
Do not put federation enrollment tokens in .kata.toml.
Hosted mode¶
When PORT is set and no explicit listener is configured, a foreground daemon
binds 0.0.0.0:$PORT. Hosted mode still requires daemon API auth and explicit
private-network trust. See Hosted mode.
[plane_sync] sets daemon-owned api_origin, web_origin, and token_env.
Cloud defaults are https://api.plane.so, https://app.plane.so, and
KATA_PLANE_TOKEN. A self-hosted API origin also becomes the default web origin.
Origins must be root HTTPS origins, or HTTP on literal loopback IPs. API keys
are resolved once per run and sent only to the configured API origin through
X-API-Key; redirects are rejected. Binding requests cannot choose origins or
credentials. Workspace, project UUID, cutoff, interval, and title presentation
are set with kata sync plane enable. See Plane sync.
Linear credentials¶
[linear_sync] selects daemon-owned token_env (default KATA_LINEAR_TOKEN)
and auth_type (api-key by default, or oauth for an operator-managed Bearer
token). All requests use https://api.linear.app/graphql; redirects are rejected.
Kata does not refresh OAuth tokens. Workspace/team UUIDs and an optional project
restriction belong to kata sync linear enable. See
Linear sync.
Twenty credentials¶
[twenty_sync] selects daemon-owned api_origin, web_origin, and token_env.
Defaults are https://api.twenty.com, https://app.twenty.com, and
KATA_TWENTY_TOKEN. Self-hosted HTTPS root origins and literal-loopback HTTP are
supported; an omitted web origin follows a custom API origin. Keys are captured
once per run and used as bearer credentials only at the configured API origin;
redirects are rejected. Enable discovers the key's workspace UUID. Bindings
cannot choose origins, keys, or another workspace. See Twenty sync.
Todoist credentials¶
[todoist_sync] selects daemon-owned token_env (default KATA_TODOIST_TOKEN).
Requests use https://api.todoist.com; the only alternative api_origin is
literal loopback HTTP for isolated fixture APIs. Tokens are captured once per
run, redirects are rejected, and binding requests cannot select credentials or
origins. Configure the selected project and history floor with
kata sync todoist enable. See Todoist sync.
[ticktick_sync] selects the daemon-owned token environment variable through
token_env, initially KATA_TICKTICK_TOKEN. The API origin is fixed to
https://api.ticktick.com. Project scope and status direction are selected with
kata sync ticktick enable. See TickTick sync
for access tokens, field ownership, completion writeback, and recovery limits.