Lightweight local task tracker. Single-user, FastAPI + SQLite, Tabler.io UI.
ntasker doubles as Claude's task memory -- the skill + /task command
let Claude read and drive your tracker, no copy-paste:
- "What should I work on next?" -- Claude 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: ..." -- Claude files it for you; drop a
#34anywhere later and it knows exactly which task you mean. - Finished an assigned task? Claude 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 with Claude button that opens a real interactive
Claude Code 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 Claude's questions, approve its tool prompts and interrupt it exactly as in a
terminal; it is the same claude binary with the same CLAUDE.md, skills, MCP and permissions.
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 claude CLI on PATH and a POSIX pseudo-terminal, otherwise the button
stays hidden. See docs/claude-runs.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.
# As a tool (recommended), from a local checkout:
uv tool install /path/to/ntasker
# ...or once published to PyPI:
uv tool install ntasker
ntasker init # create DB at the default platformdirs path
ntasker serve # start server on http://127.0.0.1:8766Open http://127.0.0.1:8766 in a browser.
For repo-local development:
cd /path/to/ntasker
make install # uv sync
make init # uv run ntasker init
make run # uv run ntasker serve --reloadRequired 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 ships a Claude Code skill (SKILL.md) and slash-command loader (/task <id>)
inside the package. The single source of truth lives under
src/ntasker/claude_assets/ and gets installed into ~/.claude/ via the CLI.
# Default install: writes SKILL.md, task.md, _ntasker_loader.py
ntasker install-claude-assets
# Status check (exit codes: 0=identical, 1=drift, 2=not installed)
ntasker install-claude-assets --check
# Update after a version bump (creates timestamped .bak.YYYYMMDD-HHMMSS backups)
ntasker install-claude-assets --force
# Use a different slash command name (e.g. /todo instead of /task)
ntasker install-claude-assets --command-name todo
# Dry run -- show planned actions without writing
ntasker install-claude-assets --dry-run
# Test/sandbox: redirect to a non-default Claude home
ntasker install-claude-assets --claude-home /tmp/test-home
# Or via env: NTASKER_CLAUDE_HOME=/tmp/test-home ntasker install-claude-assetsThe --command-name flag accepts only [A-Za-z0-9_-]+ (no slashes, no dots) to
prevent path traversal. The helper file _ntasker_loader.py always keeps that
exact name regardless of the slash command.
ntasker serve prints a one-liner to stderr at boot if installed Claude assets
are out of date relative to the running version. The /settings UI shows the
same status as a read-only card; there is intentionally no HTTP write endpoint
(installs are user-initiated via the CLI to avoid CSRF / DNS-rebinding write
surface).
| 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 |
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 install-claude-assets |
Install / check the Claude Code skill + /task slash-command |
ntasker assets fetch / status / remove |
Manage the optional local vendor-asset cache |
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/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
);
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.

