Lightweight local task tracker. Single-user, FastAPI + SQLite, Tabler.io UI.
ntasker is agent-agnostic: every task can run on Claude Code, OpenCode or Pi (and the framework is extensible
to more). It doubles as your agent's task memory -- the skill + /task command let the agent
read and drive your tracker, no copy-paste:
- "What should I work on next?" -- the agent grabs the open tasks for your current project folder and ranks them by urgency.
/task 34-- pulls #34 into the session (title, description, tags), flips it to in progress, and warns you if you're sitting in the wrong project.- "Add a todo: ..." -- it files the task for you; drop a
#34anywhere later and it knows exactly which task you mean. - Finished an assigned task? The agent moves it to Review for you to sign off -- it never closes, deletes, or archives tasks on its own.
The flip side of the integration above: every task row has a run button -- showing that task's agent logo -- that
opens a real interactive session -- the genuine TUI, embedded in the page via xterm.js -- running in the task's project
directory and seeded with /task <id>. You answer the agent's questions, approve its tool prompts and interrupt it
exactly as in a terminal; it is the same CLI with the same CLAUDE.md, skills, MCP and permissions.
Each task picks its agent (or inherits the default_agent setting); the run button only appears when that agent's CLI
resolves. Sessions run in the background (the button shows a spinner, and re-opening reattaches to the live session);
marking a task done ends its session. Needs the agent's CLI on PATH (or a configured path) and a POSIX
pseudo-terminal, otherwise the button stays hidden. See docs/claude-runs.md and
docs/agents.md.
- Backend: FastAPI + uvicorn, Python stdlib
sqlite3 - Frontend: HTML + AlpineJS + Tabler.io. Default = jsDelivr CDN at runtime, with
SRI hashes pinned in
src/ntasker/assets.py. Optional fully-offline mode viantasker assets fetch(writes into the user-data dir, never into the Python wheel). No build step. - Storage: SQLite at
platformdirs.user_data_dir("nTasker")/tasks.dbby default (Linux:~/.local/share/nTasker/tasks.db) - Layout: PyPA src-layout, package
src/ntasker/, entry pointntasker = ntasker.cli:main
Default 127.0.0.1:8766. Do not expose this on a network -- there is no auth.
Override via ntasker serve --host <h> --port <p> if you really need to.
This is a personal local tool, not a multi-user service.
Highest precedence wins:
--db <path>flag on every CLI invocation.- Environment variable
NTASKER_DB. platformdirs.user_data_dir("nTasker") / "tasks.db"(default).
Per-OS defaults:
| OS | Path |
|---|---|
| Linux | ~/.local/share/nTasker/tasks.db |
| macOS | ~/Library/Application Support/nTasker/tasks.db |
| Windows | %LOCALAPPDATA%\nTasker\tasks.db |
Only the Linux path is regularly tested; the others are derived via platformdirs.
Install from PyPI, then run it as a background service that starts at login and restarts on crash -- systemd --user
on Linux, launchd on macOS. User-scoped, no root:
uv tool install ntasker # install from PyPI
ntasker service install --auto-update # run as a service + daily auto-updateOpen http://127.0.0.1:8766 in a browser. That's it -- the service creates the database on first start, restarts on crash, and keeps itself up to date.
On Linux, run this once so the service survives logout:
loginctl enable-linger $USERManage it later:
ntasker service status # install + active state
ntasker service start / stop # start / stop the installed service
ntasker self-update # upgrade from PyPI now, then restartFull reference (uninstall, restart, update_command override, scheduling): docs/service.md.
No supervisor -- just run the server until you close it:
ntasker serve # http://127.0.0.1:8766, Ctrl-C to stopcd /path/to/ntasker
make install # uv sync
make run # uv run ntasker serve --reloadFor a global ntasker command that runs live from your working tree -- edits take effect immediately, no rebuild --
install it editable as a uv tool:
uv tool install -e . # global `ntasker`, live from src/This is independent of the PyPI install above; the two compete for the same ~/.local/bin/ntasker symlink and the same
ntasker.service unit, so use one or the other as your active setup. To validate the real PyPI install without
disturbing your repo setup, install it into a throwaway venv instead.
Required for the project sidebar to populate: configure where your project symlinks live.
Via UI: open /settings in the browser, fill in projects_dir, save.
Via CLI: ntasker config set projects_dir ~/Projekte
Via ENV: NTASKER_PROJECTS_DIR=/path/to/projects ntasker serve (overrides the DB value).
The validator requires the path to be absolute, exist, be a directory, and be readable.
ntasker tracks a directory. Each immediate subdirectory (or symlink to a
project repo) inside projects_dir is exposed as a selectable Project in
the UI sidebar and the project= API filter. Tasks can be assigned to
one of these projects (by folder/symlink name) or stay cross-project
(null). The directory listing is read on demand on every request --
there is no scan job and no DB-cached project list. Add or remove a
folder/symlink in projects_dir and it shows up (or disappears) on the
next reload.
ntasker ships with English (default) and German UI strings. Translation
uses the Python stdlib gettext module; catalogs live at
src/ntasker/locale/<lang>/LC_MESSAGES/ntasker.{po,mo}.
Pick the UI language via the language setting:
| Value | Behaviour |
|---|---|
auto |
Parse the Accept-Language HTTP header; fallback English. Default. |
en |
Always English. |
de |
Always German. |
ntasker config set language de # pin to German
ntasker config unset language # back to auto
NTASKER_LANGUAGE=en ntasker serve # one-shot ENV overrideCLI follows: setting > LANG/LC_MESSAGES env > English.
For development, regenerate catalogs after touching strings:
make i18n # extract + update + compile
make i18n-init-de # bootstrap a fresh language (idempotent)Extraction uses Babel (dev-only dep; runtime
needs only the stdlib). Catalog keywords: _, _lazy, t (Jinja
shorthand), N_ (no-op marker for module-level constants).
Tabler core CSS, Tabler-Icons webfont, and Alpine.js are loaded from
jsDelivr by default with SRI
hashes pinned in src/ntasker/assets.py. The wheel ships no vendor
binaries -- it stays under 100 KB.
For offline use, populate the user-data cache once:
ntasker assets fetch # downloads + verifies SRI for each manifest entry
ntasker assets status # shows mode + per-asset state
ntasker assets remove --yes # wipes the cacheThe cache lives at platformdirs.user_data_dir("nTasker") / "vendor"
(Linux: ~/.local/share/nTasker/vendor). Mode selection is via the
assets_mode setting:
| Value | Behaviour |
|---|---|
cdn |
always load from jsDelivr (with SRI) |
local |
always load from the user-data cache (must run assets fetch) |
auto |
local if cache is complete, else CDN. Default. |
ENV override: NTASKER_ASSETS_MODE=cdn ntasker serve. SRI is emitted in
both modes (catches on-disk tampering for local too).
ntasker is agent-agnostic -- it integrates with Claude Code, OpenCode and Pi, and adding another agent is one
registry entry. The agent registry in src/ntasker/agents.py (one AgentSpec per agent) is the single source of
truth for the binary, the spawn command, the config home, and the icon.
Each task carries an agent (a nullable field). NULL falls back to the default_agent setting, then to claude.
Pick it in the new-task form, the edit dialog, or via the CLI:
ntasker add --title "..." --agent opencode # create a task pinned to OpenCode
ntasker patch 34 --agent pi # repoint a task
ntasker patch 34 --agent '' # clear -> falls back to default_agent
ntasker config set default_agent opencode # change the default for new tasksntasker ships a skill (SKILL.md) and slash-command loader (/task <id>) inside the package and installs them into
each agent's own config home -- Claude ~/.claude, OpenCode ~/.config/opencode, Pi ~/.pi/agent:
ntasker agent list # all agents: CLI availability + integration status
ntasker agent install opencode # install the SKILL.md + /task slash command
ntasker agent install pi --check # status check: exit 0=identical, 1=drift, 2=not installed
ntasker agent install claude --force # update after a version bump (timestamped backups)
ntasker agent install opencode --dry-run # show planned actions without writing
ntasker agent install pi --command-name todo # use /todo instead of /task
ntasker agent install claude --home /tmp/test-home # redirect to a non-default config homeinstall-claude-assets remains as a deprecated alias of ntasker agent install claude. The --command-name flag
accepts only [A-Za-z0-9_-]+ (no slashes, no dots) to prevent path traversal.
Configurable CLI path. When the server runs with a narrower PATH than your shell (e.g. a systemd --user unit
without nvm), point ntasker at an agent's CLI with the per-agent claude_bin / opencode_bin / pi_bin setting
(ENV NTASKER_CLAUDE_BIN etc.). Empty auto-detects on PATH.
ntasker serve prints a one-liner to stderr at boot if installed assets are out of date relative to the running
version. The /settings UI shows the same status as read-only cards (one per agent under an AI agent integration
card); there is intentionally no HTTP write endpoint (installs are user-initiated via the CLI to avoid CSRF /
DNS-rebinding write surface). Full reference: docs/agents.md.
| Command | What it does |
|---|---|
ntasker init |
Create / migrate the schema at the active DB path |
ntasker serve |
Run the FastAPI server (defaults: 127.0.0.1:8766) |
ntasker list [filters] |
List tasks; supports --project, --tag, --phase, ... |
ntasker show <id> |
Show a single task; pair with --json for raw output |
ntasker add --title=... |
Create a task; optional --project --phase --priority --tag --agent |
ntasker done <id> |
Mark a task as done |
ntasker patch <id> [...] |
Patch arbitrary fields (--title, --phase, --status, ...) |
ntasker tag-add <id> <t> |
Append a tag |
ntasker tag-rm <id> <t> |
Remove a tag |
ntasker stats [filters] |
Tab counts (open/done/archive) honoring filters |
ntasker config list |
Show all settings |
ntasker config get <k> |
Read a setting |
ntasker config set <k> <v> |
Write a setting (validated) |
ntasker config unset <k> |
Remove a setting |
ntasker agent list |
List agents with CLI availability + /task integration status |
ntasker agent install <key> |
Install / check an agent's skill + /task slash-command (claude/opencode/pi) |
ntasker assets fetch / status / remove |
Manage the optional local vendor-asset cache |
ntasker service install / uninstall / status / start / stop |
Run ntasker as an OS service (systemd / launchd) |
ntasker self-update |
Upgrade the package from PyPI, then restart the service |
Global flags:
--db <path>-- override the resolved DB path for this invocation.--version-- print the package version and exit.
Most listing commands accept --json for machine-readable output.
make smokeRuns an in-process FastAPI test client against a temp DB and exercises a couple of CLI subcommands via subprocess.
| Method | Path | Notes |
|---|---|---|
| GET | / |
The single-page task UI |
| GET | /settings |
The settings UI |
| GET | /api/changes |
Cheap change token ({v} = DB file mtime in ns). The UI polls it and refetches only when it changed, so CLI/API writes surface live. See docs/live-updates.md. |
| GET | /api/projects |
[{name, open_count}], __none__ first; sets X-Settings-Missing: projects_dir if unconfigured |
| GET | /api/tags |
[{name, open_count}], sorted by open_count DESC, name ASC |
| POST | /api/tags/cleanup |
Delete dangling tags (no task_tags row). Returns {removed, removed_names}. Idempotent. |
| GET | /api/phases |
[{value, label, open_count}], fixed workflow order: wip, planned, later, __none__ |
| GET | /api/priorities |
[{value, label, open_count}], fixed order: critical, high, normal, low |
| GET | /api/tasks |
Filters: project (multi), tag (multi, OR), phase (multi, OR; __none__ = phase IS NULL), priority (multi), status, archived, search. Filters across params combine with AND. |
| GET | /api/tasks/{id} |
Single task incl. tags |
| GET | /api/stats |
Tab counts (open/done/archive), respects all filters |
| POST | /api/tasks |
{project?, title, description?, phase?, priority?, tags?} |
| PATCH | /api/tasks/{id} |
Any subset of {title, description, project, phase, priority, status, archived, tags} -- tags is a full replace |
| DELETE | /api/tasks/{id} |
Hard delete (the UI archives by default) |
| GET | /api/settings |
List all settings rows |
| GET | /api/settings/{key} |
Single setting or 404 |
| PUT | /api/settings/{key} |
{value: "..."} -- 200 on accept, 400 if a registered validator rejects |
| DELETE | /api/settings/{key} |
204 on success, 404 if not present |
| GET | /api/agents |
Read-only registry feed: per-agent availability + /task integration status, plus the default |
| GET | /api/claude-assets/status |
Read-only: {installed, drift, package_version, claude_home, files[]} |
OpenAPI: http://127.0.0.1:8766/api/docs
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project TEXT,
title TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'open',
phase TEXT,
priority TEXT NOT NULL DEFAULT 'normal',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
completed_at TEXT,
archived INTEGER NOT NULL DEFAULT 0,
agent TEXT -- AI agent for this task; NULL = default_agent setting (then claude)
);
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE
);
CREATE TABLE task_tags (
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (task_id, tag_id)
);
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);status: open | done. phase: wip | planned | later | NULL.
priority: critical | high | normal | low (NOT NULL, default normal).
Tag names are normalised to lowercase on write; UNIQUE COLLATE NOCASE keeps it tidy.
- DB init on startup; pure idempotent
CREATE TABLE IF NOT EXISTS. Legacy columns are dropped or added intry/except OperationalErrorblocks -- no Alembic, no migration files. - All SQL parameterised (
?); no string interpolation. - Project list is read live each request from the symlinks under the
configured
projects_dir-- no caching. - Sidebar
open_countvalues are absolute (always count all open + non-archived tasks), so toggling filters does not flicker the sidebar. - Hard-delete is intentionally rare; archive is the default. Deleting a task
cascades through
task_tagsbut leavestagsrows in place (zero-cost dangling). - Project / phase / tag / priority badges in a task row are clickable: each one
toggles the matching filter.
@click.stopprevents the parent row interactions. - Dates stored as UTC ISO strings, rendered locally via
Intl.RelativeTimeFormat('de-DE').
GitHub: https://github.com/nerdocs/ntasker
See CHANGELOG.md. Highlights:
- 1.2.0 -- Packaged Claude Code assets generalised (no user-specific routing/paths). AGPL-3.0-or-later license. README explains
projects_dirsemantics./taskaccepts#-prefix. Task-ID click copies/task #<id>to clipboard. Existing installs needntasker install-claude-assets --forceafter upgrade. - 1.1.0 --
install-claude-assetsCLI for shipping the Claude Code skill +/taskslash-command from the package; read-only/api/claude-assets/statusendpoint and Settings UI card; boot drift warning. - 1.0.0 -- Renamed
nerdocs-tracker->ntasker; src-Layout; CLI with subcommands; settings module + UI; configurableprojects_dir; DB moved toplatformdirsdefault. Breaking. - 0.4.0 --
priorityfield with sidebar filter and badge. - 0.3.x -- Cache-buster, version badge, archive button polish.
Licensed under the GNU Affero General Public License, version 3 or later
(AGPL-3.0-or-later). See LICENSE for the full text.
The Affero clause means: if you run a modified version of nTasker as a network service, you must offer the modified source code to its users. For local single-user use this has no practical impact.

