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
queues the task and 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 the task. 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.
The queue is the only way a session starts: every run button puts its task at the head of its project's lane, and
ntasker works through the queue unattended, one task per project at a time, taking the next one as soon as the previous
task is done -- closed by you after review, or by the agent when the task told it to. done is the only thing that
makes ntasker end a session. Pause stops new starts; running tasks keep going.
The panel shows one column per project, because that is what runs in parallel. To make one task wait for another -- across projects too -- drop it on the middle of the other; the edges keep reordering. See docs/task-queue.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.
Projects sharing a name prefix (thrito, thrito-meta, ...) fold into one
family; the row menu and drag-and-drop override the rule, and projects can be
hidden. Full reference: docs/projects.md.
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
plugin. The agent registry in src/ntasker/agents.py (one AgentSpec per agent, contributed by
src/ntasker/plugins/<key>/) 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. Auto-detection looks on PATH, then in the conventional install dirs (~/.local/bin,
~/.claude/local, ~/.opencode/bin, Homebrew, /usr/local/bin, nvm-managed Nodes), so a systemd --user unit or
launchd agent with a narrow PATH still finds the CLI. For installs elsewhere, set the per-agent claude_bin /
opencode_bin / pi_bin setting (ENV NTASKER_CLAUDE_BIN etc.). Empty auto-detects.
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.
Agent integrations and optional features are plugins that can be switched off individually: /settings -> Plugins, ntasker config set plugins_disabled '["pi"]', or
NTASKER_PLUGINS_DISABLED=pi,opencode. A disabled plugin's routes 404, its agent is neither listed nor resolvable, and
its data stays intact. Contract and slots: docs/plugins.md. Built-in feature plugins:
task_context -- attach files, notes, personas, skills and MCP servers to a task, handed to the agent in its
briefing (docs/task-context.md); workspace -- team (Claude Code subagents), skills, knowledge
base and documents on a /workspace page and in the sidebar, plus a viewer (docs/workspace.md);
voice (opt-in: ntasker enable voice installs the ntasker[voice] extra; models are downloaded from a catalog
on /settings) -- dictate task descriptions with local speech recognition (docs/voice.md).
Coming from the drfoehn fork? See docs/migrating-from-fork.md.
| Command | What it does |
|---|---|
ntasker init |
Create / migrate the schema at the active DB path |
ntasker serve / start |
Run the FastAPI server (defaults: 127.0.0.1:8766); start is an alias |
ntasker stop |
Shut down a running server over HTTP (POST /shutdown) |
ntasker restart |
Restart the server: via the installed service if there is one, else stop + start detached. --foreground keeps it in your terminal |
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 queue list |
Show the auto-run queue in run order, plus its on/off state |
ntasker queue add <id...> |
Queue tasks (--top inserts at the front); an already-queued id moves |
ntasker queue rm <id...> |
Take tasks out of the queue |
ntasker queue clear |
Empty the queue |
ntasker queue start / pause |
Let the queue work through its tasks, or stop starting new ones |
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 enable <plugin> |
Switch a plugin on; installs its extra's packages if missing |
ntasker disable <plugin> |
Switch a plugin off |
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 |
ntasker completion <shell> |
Print the bash/zsh completion script; --install / --uninstall hook it into the shell rc |
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.
ntasker completion bash --install (or zsh) writes a completion script to the user-data dir and sources it from
~/.bashrc (~/.bash_profile on macOS) / ~/.zshrc; the same switch lives under Settings → Maintenance. The
script is generated from the CLI's own argument tree and refreshed on every server start, so new subcommands
(including plugin ones) complete without a reinstall. Without installing: eval "$(ntasker completion bash)".
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/queue |
{enabled, items[]} -- the auto-run task queue in run order. See docs/task-queue.md. |
| PUT | /api/queue |
{ids: [...]} replaces the whole queue, head first. Closed / archived / missing ids are dropped. |
| GET | /api/agents |
Read-only registry feed: per-agent availability + /task integration status, plus the default |
| GET | /api/plugins |
Built-in plugins + enabled flag; toggle via PUT /api/settings/plugins_disabled / plugins_enabled |
| WS | /api/voice/ws |
Voice plugin: 16 kHz PCM in, partial / final text out (docs/voice.md) |
| GET/POST | `/api/voice/models[/{name} | /job]` |
| GET/POST/DELETE | /api/tasks/{id}/context[/{cid}] |
Attachments (docs/task-context.md) |
| GET/PUT/POST | `/api/workspace[/file | browse |
| 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.

