· · 14 min read Beginner

Taskwarrior, Timewarrior and Vit: The Ultimate CLI Productivity Stack

Complete tutorial for taskwarrior, timewarrior and vit. From basics to advanced: projects, dependencies, time tracking, taskd sync server, and vim-style task management.

A terminal with task cards and a stopwatch, one keystroke moves a card, a sticky-note monster collapses on the desk

TL;DR Taskwarrior, timewarrior and vit are my task list, my time tracking and the vim-style screen on top, all in the terminal and all plain text in ~/.task. Two commands get you started. The rest of this post is what I bolted on afterwards, from projects and dependencies to a self-hosted sync server, and you can stop at any section.

For context on how my brain works, see Working with an AuDHD Brain.

My whole productivity system fits in two lines:

task add "Write the intro" +next
task 1 start

That’s it. The task is written down, the clock is running, and both happened in under a second without me touching the mouse. The stack behind it is taskwarrior for tasks, timewarrior for time, and vit as a vim-style screen on top of the first one. Keyboard first, and plain text on disk.

No Electron apps. No cloud sync shipping my data to a vendor I can’t inspect. The files sit in ~/.task and they’re mine.

Why this stack

I fought with task management for years. Todoist, Things, Notion, a pile of Obsidian plugins. I tried them all and dropped them all, and the reason was the same every time. Getting a thought out of my head and into the tool took longer than the thought lasted.

For my brain that’s the whole problem. If writing a task down costs me three clicks and a loading spinner, the task doesn’t get written down. A to-do list with a loading spinner is something I resent on principle. Taskwarrior takes under a second, needs no mouse, works offline without a server, plugs into any script I care to write, and keeps its data in files I can open with any editor.

Is there a catch? There’s a learning curve, and the first week feels clumsy. I think it pays for itself within days. The rest of this post is everything I added to those two lines, from task add to a sync server I host myself.

Installation

macOS

brew install task timewarrior vit

Linux (Debian/Ubuntu)

sudo apt install taskwarrior timewarrior
pip install vit  # or via package manager

Arch Linux

sudo pacman -S task timew vit

After installing, run task once so it creates the config:

task version
# Creates ~/.taskrc and ~/.task/

Add, list, modify, start

Adding tasks

# Simple task
task add "Review pull request"

# With tags
task add "Fix login bug" +urgent +backend

# With due date
task add "Report delivery" due:friday

# With project
task add "Write API endpoint" project:acme.api

# All together
task add "Deploy to production" project:acme.api +urgent due:tomorrow priority:H

Viewing tasks

task list              # All pending tasks
task                   # Same as task list (or task next)
task all               # Including completed and deleted
task completed         # Only finished tasks
task +urgent           # Filtered by tag
task project:acme      # Filtered by project

Managing tasks

task 1 done            # Mark as done
task 1 delete          # Delete
task 1 modify +tag     # Add tag
task 1 modify due:monday  # Change due date
task 1 edit            # Open in $EDITOR
task 1 info            # All details

Starting and stopping tasks

task 1 start           # Mark as "active"
task 1 stop            # No longer active
task +ACTIVE           # View active tasks

Projects

Projects in taskwarrior are hierarchical, with dots between the levels:

# Project structure
task add "Something" project:client.project.subproject

# Examples
task add "Design API" project:acme.api
task add "Write tests" project:acme.api.backend
task add "Build UI" project:acme.api.frontend

Viewing projects

# All tasks in a project (including subprojects)
task project:acme list

# Only specific project
task project:acme.api list

# Project summary
task projects

Contexts narrow the view

A context is a filter that stays on until you switch it off:

# Define context
task context define work "project:acme or project:byteherder"
task context define acme "project:acme"

# Activate context
task context acme
# Now 'task list' only shows acme tasks

# View context
task context show

# Disable context
task context none

That’s what my p function does. The full implementation is in my time-system scripts.

Dependencies

Tasks can depend on each other:

# Task 2 depends on task 1
task 2 modify depends:1

# Multiple dependencies
task 3 modify depends:1,2

# When creating
task add "Deploy" depends:1,2

Blocked tasks

# View what's blocked
task blocked

# View what's unblocked (ready to do)
task unblocked

# View the dependency chain
task 1 info
# Shows: "blocking: 2, 3" and "blocked by: (none)"

A worked example

task add "Design database schema"           # Task 1
task add "Build API endpoints" depends:1    # Task 2
task add "Frontend integration" depends:2   # Task 3
task add "Write tests" depends:1            # Task 4
task add "Documentation" depends:2,3,4      # Task 5

task list
# ID Dep Description
#  1     Design database schema
#  2   1 Build API endpoints      [blocked]
#  3   2 Frontend integration     [blocked]
#  4   1 Write tests              [blocked]
#  5 2-4 Documentation            [blocked]

task 1 done
# Now tasks 2 and 4 are unblocked

task list
# ID Dep Description
#  2     Build API endpoints
#  3   2 Frontend integration     [blocked]
#  4     Write tests
#  5 2-4 Documentation            [blocked]

Tags

Tags are labels you make up as you go:

# Adding
task add "Something" +urgent +backend +review

# Filtering
task +urgent list
task +backend +review list    # AND
task +backend or +frontend    # OR
task -urgent                  # NOT (exclude)

# Remove tag
task 1 modify -urgent

Tag conventions

+next        # Next to do
+urgent      # High priority
+waiting     # Waiting on someone else
+review      # Needs review
+blocked     # Manually blocked (besides depends)
+quick       # < 15 minutes

Priorities

Three levels, and I’ve never wanted a fourth:

# Three levels: H(igh), M(edium), L(ow)
task add "Critical fix" priority:H
task add "Nice to have" priority:L

# Modify
task 1 modify priority:M

# Filter
task priority:H list

Due dates

Taskwarrior reads dates the way you’d type them to a colleague:

# Specific
task add "Meeting" due:2026-01-30
task add "Deadline" due:2026-01-30T14:00

# Relative
task add "Soon" due:tomorrow
task add "This week" due:friday
task add "In a week" due:1week
task add "End of month" due:eom
task add "In 3 days" due:3d

# Recurring
task add "Weekly review" due:monday recur:weekly
task add "Standup" due:tomorrow recur:daily until:2026-12-31

Viewing by due date

task due:today list      # Today
task due.before:friday   # Before friday
task overdue             # Past due
task +OVERDUE            # Same

Reports

Taskwarrior ships with a report for each question you ask most:

task list         # Default list
task next         # Next tasks (smartly sorted)
task ready        # Ready to do (unblocked, not waiting)
task blocked      # Blocked by dependencies
task waiting      # Waiting on something
task recurring    # Recurring tasks
task completed    # Finished
task all          # Everything

Custom reports

Define your own in ~/.taskrc:

# Minimal list
report.minimal.columns=id,description
report.minimal.labels=ID,Description
report.minimal.filter=status:pending

# Then: task minimal

Timewarrior integration

Timewarrior tracks time on its own, and with one hook it follows every task start and task stop too.

Manual tracking

timew start "coding" +projectX    # Start tracking
timew stop                        # Stop
timew                             # Current status
timew summary                     # Today
timew summary :week               # This week
timew summary :month              # This month

The taskwarrior hook

This is the hook that ties the two together:

# ~/.task/hooks/on-modify.timewarrior
#!/usr/bin/env python3
import json
import subprocess
import sys

old = json.loads(sys.stdin.readline())
new = json.loads(sys.stdin.readline())

if 'start' in new and 'start' not in old:
    project = new.get('project', 'untagged')
    tags = new.get('tags', [])
    subprocess.run(['timew', 'start', project] + tags)

if 'start' in old and 'start' not in new:
    subprocess.run(['timew', 'stop'])

print(json.dumps(new))
chmod +x ~/.task/hooks/on-modify.timewarrior

Now task 1 start starts the clock as well.

Timewarrior reports

timew summary :day              # Today
timew summary :week             # This week
timew summary 2026-01-20 - today  # Range
timew tags                      # All tags
timew summary :week project:acme  # Filtered

Correcting time

Forgot to start the clock? Fix it after the fact:

# Adjust current tracking
timew modify start -30min       # Started 30 min earlier

# Adjust historical entry
timew summary :ids              # Find ID
timew modify @1 start 09:00     # Adjust start
timew delete @1                 # Delete entry

Vit, a vim interface for taskwarrior

Vit is a TUI with vim keybindings. If your fingers already know j, k and /, it needs no manual.

Starting

vit                    # Default view
vit project:acme       # Filtered view
KeyAction
j / kUp / down
ggGo to beginning
GGo to end
Ctrl+d / Ctrl+uHalf page down/up
/Search
n / NNext/previous match

Task actions

KeyAction
aAdd task
eEdit task (in $EDITOR)
mModify (inline)
dDone
DDelete
sStart/stop
uUndo
EnterTask info

Modify examples

Press m and type what you’d type after task modify:

project:acme              # Set project
+urgent                   # Add tag
-waiting                  # Remove tag
due:friday                # Set due date
depends:1                 # Add dependency
priority:H                # Set priority

Switching views

KeyView
1next
2list
3completed
4blocked
5-9Custom reports

Command mode

KeyAction
:Type task command directly
:qQuit
:!commandShell command

:project:acme +urgent filters the list live, for example.

Vit config

~/.vit/config.ini:

[vit]
default_keybindings = vi

[report]
default_report = next

[color]
color = on

My daily workflow

All my aliases, functions and scripts sit in one repo, time-system:

# Install
git clone https://github.com/kapott/time-system.git
cd time-system && ./install.sh
source ~/.zshrc

One directory per project

The scripts expect two levels of directories, client and then project:

~/Documents/Git/           # TASKFUNC_PROJECTS_DIR
├── clientA/
│   ├── project1/
│   └── project2/
├── clientB/
│   └── projectX/

A day, start to finish

# Morning
p                        # Select client/project with fzf
                         # -> cd's to directory
                         # -> sets taskwarrior context
                         # -> shows pending tasks
vit                      # View tasks in TUI

# Working
tstart                   # Pick task with fzf, starts timer
# ... work ...
tstop                    # Stop task + timer

# Throughout the day
tadd "New task"          # Adds to current project (or inbox if no scope)
tpl                      # List tasks for current project
tnext                    # What's next?

# End of day
dayend                   # Summary: completed, time tracked, still active

The commands I use most

CommandDescription
pSelect project, cd to dir, set scope
tstart [id]Start task + timer (fzf if no id)
tstopStop current task + timer
tadd <desc>Add task to current project
tdone [id]Complete task (fzf if no id)
tplList tasks for current project
tctxnClear project scope
weekstartWeekly review
dayreviewDaily review
dayendEnd of day summary
twcurrentCurrent tracking status

The full command reference lists every alias and function.

Backup

Everything lives in ~/.task/, so a backup is a copy:

# Simple backup
cp -r ~/.task ~/.task-backup-$(date +%Y%m%d)

# Or with git
cd ~/.task && git init && git add -A && git commit -m "Backup"

Custom fields and urgency

User defined attributes

Custom fields, which taskwarrior calls UDAs:

# In ~/.taskrc
uda.estimate.type=duration
uda.estimate.label=Est

# Usage
task add "Big job" estimate:2h
task estimate.over:1h list

Urgency tuning

Taskwarrior computes urgency on its own, and the coefficients are yours to change:

# ~/.taskrc
urgency.user.tag.urgent.coefficient=5.0
urgency.user.tag.quick.coefficient=2.0
urgency.blocking.coefficient=8.0
urgency.blocked.coefficient=-5.0

Taskd, the sync server

Use taskwarrior on more than one machine and you’ll want them to agree. Taskd is the official sync server for that, and you host it yourself, so the data stays on a machine you control.

What taskd gives you

The same tasks on every machine, merge conflicts handled by taskwarrior itself, offline first with a sync whenever the server is reachable, and a server that’s yours.

Alternatives

Before you set up taskd, look at the alternatives:

OptionComplexityAdvantage
SyncthingLowSimple, sync entire ~/.task folder
GitLowVersion control, but no real sync
TaskdMediumOfficial, proper merge handling
Taskchampion Sync ServerMediumNewer, for Taskwarrior 3.x

For most people Syncthing is the easiest option. Taskd is for when you want a proper merge instead of last write wins.

Installing the taskd server

Via package manager

# Debian/Ubuntu
sudo apt install taskd

# Arch
sudo pacman -S taskd

# macOS (as server, unusual)
brew install taskd
# docker-compose.yml
version: '3'
services:
  taskd:
    image: andir/taskd
    ports:
      - "53589:53589"
    volumes:
      - ./taskd-data:/var/taskd
    environment:
      - CERT_CN=taskd.example.com
      - CERT_ORGANIZATION=MyOrg
docker-compose up -d

Server configuration

If you install taskd by hand:

# Create data directory
export TASKDDATA=/var/taskd
sudo mkdir -p $TASKDDATA
sudo chown $USER:$USER $TASKDDATA

# Initialize
taskd init

# PKI setup (certificates)
cd /usr/share/taskd/pki  # or where your pki scripts are
./generate

# Copy certificates
cp client.cert.pem $TASKDDATA
cp client.key.pem $TASKDDATA
cp server.cert.pem $TASKDDATA
cp server.key.pem $TASKDDATA
cp server.crl.pem $TASKDDATA
cp ca.cert.pem $TASKDDATA

# Configure server
taskd config --force client.cert $TASKDDATA/client.cert.pem
taskd config --force client.key $TASKDDATA/client.key.pem
taskd config --force server.cert $TASKDDATA/server.cert.pem
taskd config --force server.key $TASKDDATA/server.key.pem
taskd config --force server.crl $TASKDDATA/server.crl.pem
taskd config --force ca.cert $TASKDDATA/ca.cert.pem
taskd config --force server localhost:53589
taskd config --force log /var/log/taskd.log
taskd config --force pid.file /var/run/taskd.pid

Organisations, groups and users

Taskd is organised in organisations, optional groups, and users:

# Create organizations
taskd add org Work
taskd add org Home
taskd add org Freelance

# Groups within organizations (optional)
taskd add group Work backend-team
taskd add group Work devops-team

# Users per organization
taskd add user Work tom
taskd add user Work jan
taskd add user Home tom
# Output: New user key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# SAVE THIS KEY!

One database per user, no sharing

This is a common misunderstanding, so it gets its own heading. Taskd doesn’t share tasks between users.

Each user gets their own separate task database. Tom’s tasks are invisible to Jan, even inside the same organisation. What taskd does is let Tom sync his own tasks between laptop, desktop and phone, and let Jan do the same with his devices. They share one server and nothing else.

So no shared projects, no team task management, no assigning a task to a colleague. If you need that anyway, these are the ways around it:

OptionHowDownside
Shared accountEveryone uses same credentialsNo audit trail, who did what?
Export/importtask export | ssh colleague task importManual, no real sync
Different toolVikunja, Kanboard, PlaneNot taskwarrior

My advice is to use taskwarrior for your own tasks and a dedicated tool for the team. Don’t try to make taskd do something it wasn’t designed for.

Generating client certificates

cd /usr/share/taskd/pki
./generate.client username

# This creates:
# - username.cert.pem
# - username.key.pem

Copy ca.cert.pem, username.cert.pem and username.key.pem to the client machine.

Configuring the taskwarrior client

On every machine that should sync:

# Place certificates in ~/.task
cp ca.cert.pem ~/.task/
cp username.cert.pem ~/.task/
cp username.key.pem ~/.task/

# Configure in ~/.taskrc
task config taskd.certificate ~/.task/username.cert.pem
task config taskd.key ~/.task/username.key.pem
task config taskd.ca ~/.task/ca.cert.pem
task config taskd.server taskd.example.com:53589
task config taskd.credentials 'MyOrg/username/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

Starting the server

# Foreground (for testing)
taskdctl start

# As systemd service
sudo systemctl enable taskd
sudo systemctl start taskd

Synchronising

# First sync (push everything to server)
task sync init

# Normal sync
task sync

Automatic sync

A hook syncs after every command, cron syncs on a schedule. The hook first:

# ~/.task/hooks/on-exit-sync (automatically after every task action)
#!/bin/bash
task sync
chmod +x ~/.task/hooks/on-exit-sync

Or, more conservatively, cron:

# Sync every 30 minutes
*/30 * * * * /usr/bin/task sync >> /tmp/tasksync.log 2>&1

Checking sync status

task sync                    # Sync and show status
task diagnostics | grep -A5 Sync  # View sync config

Troubleshooting taskd

If sync says “Certificate fails validation”, check the certificates against the CA:

# Check if certificates are correct
openssl verify -CAfile ~/.task/ca.cert.pem ~/.task/username.cert.pem

If it says “Could not connect to server”, check that the server is up and the port is open:

# Check if server is running
nc -zv taskd.example.com 53589

# Check firewall
sudo ufw allow 53589/tcp

Taskwarrior resolves most “Sync conflict” messages itself. If it doesn’t:

task sync     # View conflict messages
task list     # Check if data is correct

Syncthing as the alternative

If taskd is more than you want, Syncthing does the job:

# Install Syncthing on both machines
# Sync the ~/.task folder
# Done

# Downside: no real merge, last write wins
# Advantage: works for everything, not just taskwarrior

My own ~/.task syncs over Syncthing. It’s simple and it works.

Troubleshooting

“No matches”

Usually a context is still active:

# Check your context
task context show

# Clear context
task context none

A task that seems gone

It’s probably completed or deleted, and both are still on disk:

task all                 # View everything
task status:completed    # Check completed
task status:deleted      # Check deleted

Undo

task undo               # Undo last action

Two commands are enough

The two lines at the top are still my whole system. Everything after them, projects, dependencies, the clock, vit, the sync server, I added once I missed it, and every one of them is optional.

If you never type more than task add and task done, you have a complete system and it’ll serve you for years. Vit earns its place when the list gets too long to read on one screen, and timewarrior the first time someone asks where your week went. The scripts come last, on the day you want it wired to your own directories. Add each one when you miss it.

There’s a learning curve, and I won’t pretend otherwise. The reason I keep coming back to this stack is that it bends to how I work instead of forcing me into someone else’s idea of a productivity app, and the data sits in plain text that I control. If you try it and it doesn’t click, fair enough. Mine has stayed.

Here’s your first task:

task add "Learn taskwarrior" +next due:today
task 1 start