Configuration
taskmd supports .taskmd.yaml configuration files for setting default options.
Config File
Create a .taskmd.yaml file in your project root or home directory:
# .taskmd.yaml
# Default directory to search for task files
dir: ./tasks
# Web server configuration
web:
# Default port for the web dashboard
port: 8080
# Bind address. 127.0.0.1 (the default) accepts connections only from this
# machine; 0.0.0.0 accepts them on every network interface.
host: 127.0.0.1
# Automatically open browser when starting the web server
auto_open_browser: false
# Disable editing (no PUT /api/tasks/{id})
readonly: falseConfig File Locations
taskmd loads exactly one config file. It searches, in order:
- The current directory, then each parent directory up to the repository root (the first directory containing
.git) ~/.taskmd.yaml
The first .taskmd.yaml found wins and the search stops. Files are not merged: when a project file exists, ~/.taskmd.yaml is ignored entirely, so a home setting such as web.port has no effect inside a project that has its own file. Running from a subdirectory still finds the project file.
--config path/to/config.yaml replaces this search with the given file.
Command-line flags and TASKMD_* environment variables override values from whichever file was loaded. See Precedence.
Supported Options
| Option | Type | Default | Description |
|---|---|---|---|
dir | string | . | Default task directory |
ignore | string[] | [] | Additional directories to skip when scanning (beyond the built-in skip list) |
worklogs | boolean | false | Allow writing worklog entries; set to true to opt in |
workflow | string | "solo" | Workflow mode: "solo" or "pr-review" |
worktree_scope | string | "unified" | unified merges task state across git worktrees; isolated keeps each worktree to its own files (details) |
todos.exclude | string[] | [] | Glob patterns to exclude from TODO/FIXME scanning |
web.port | integer | 8080 | Web server port |
web.host | string | 127.0.0.1 | Web server bind address; 0.0.0.0 binds every interface (details) |
web.auto_open_browser | boolean | false | Auto-open browser on web start |
web.readonly | boolean | false | Start web server in read-only mode (disables editing) |
id.strategy | string | "sequential" | ID generation strategy (details) |
id.prefix | string | "" | Prefix for prefixed strategy |
id.length | integer | 6 | Length of generated IDs (used by random and ulid strategies) |
id.padding | integer | 3 | Zero-padding width for sequential strategy |
effort | string[] | [small, medium, large] | Ordered effort vocabulary, lowest to highest (details) |
scopes | map | — | Scope-to-path mappings for the touches field (details) |
phases | array | [] | Phase definitions with metadata (details) |
next.unphased | string | "current" | Where next ranks tasks with no phase: current (the current phase) or last (after every phase) (details) |
projects | array | [] | Registered projects for multi-project workflows (details) (global config only) |
default_project | string | "" | Default project ID to use when no --project flag is specified (global config only) |
ignore — The scanner already skips common directories (node_modules, vendor, dist, build, .next, .nuxt, out, target, __pycache__, and hidden directories). Use ignore to add project-specific directories:
ignore:
- "tmp"
- "cache"
- "legacy"worklogs — Worklogs are opt-in; an absent key means disabled. Until you set worklogs: true, taskmd worklog <id> --add refuses to write and explains why, and agents skip worklog steps. Only writing is gated — existing worklogs can always be read regardless of this setting, and taskmd rm still deletes a removed task's worklog.
workflow — Controls the development workflow mode:
solo(default) — optimized for single-developer workflowspr-review— optimized for PR-based collaborative workflows
todos.exclude — Glob patterns to exclude files from taskmd todos scanning. These are additive with CLI --exclude flags:
todos:
exclude:
- "parser_test.go"
- "**/*_test.go"TIP
Config files are meant for project-level settings. format is CLI-only. verbose and quiet are honoured from config and environment today, but validate reports them as unknown keys; treat them as per-invocation flags.
ID Strategy Configuration
The id section controls how task IDs are generated by the add and next-id commands. Four strategies are available:
| Strategy | Format | Example ID | Description |
|---|---|---|---|
sequential | Zero-padded number | 001, 042 | Default. Finds the highest existing ID and increments. |
prefixed | Prefix + number | dr-001, jk-042 | Like sequential, but with a user-defined prefix. Useful for multi-contributor projects. |
random | Random alphanumeric | a3f9x2 | Short random string. Avoids ID collisions across branches. |
ulid | ULID | 01HGW3... | Universally Unique Lexicographically Sortable Identifier. Guarantees uniqueness with natural sort order. |
Pick exactly one id block for your .taskmd.yaml. A file with more than one id: key is not rejected; the setting is silently ignored and IDs fall back to sequential.
Sequential (default):
id:
strategy: sequential
padding: 3 # 001, 002, ... 999Prefixed (useful for teams):
id:
strategy: prefixed
prefix: "dr-" # used literally: dr-001, dr-002, ...Random (branch-safe):
id:
strategy: random
length: 6 # optional, default 6ULID (sortable and unique):
id:
strategy: ulid
length: 10 # optional, default 6Avoiding ID Collisions
When multiple contributors create tasks on separate branches, sequential IDs can collide after merge. Use prefixed (with unique per-contributor prefixes), random, or ulid to avoid this. If collisions do occur, use taskmd deduplicate to resolve them.
Scopes Configuration
The scopes key defines concrete mappings for the abstract scope identifiers used by the touches frontmatter field. The tracks command uses touches to detect spatial overlap between tasks and assign them to parallel work tracks — tasks sharing a scope are placed in separate tracks to avoid merge conflicts.
# .taskmd.yaml
scopes:
cli/graph:
description: "Graph visualization and dependency rendering"
paths:
- "apps/cli/internal/graph/"
- "apps/cli/internal/cli/graph.go"
cli/output:
paths:
- "apps/cli/internal/cli/format.go"Each scope entry has the following fields:
| Field | Required | Description |
|---|---|---|
description | No | Human-readable explanation of what the scope covers. Included in validation error messages. |
paths | No | List of file or directory paths that the scope maps to. |
Behavior:
- When scopes are configured, any
touchesvalue in a task that does not match a configured scope produces a warning. - When no scopes config exists, all
touchesvalues are accepted silently.
Worktree Scope Configuration
The worktree_scope key controls what taskmd reads in a git repository with multiple worktrees:
unified(default) — taskmd merges task state across all worktrees so each checkout sees one coherent view. The merge activates by itself only when sibling worktrees actually exist, so single-worktree repos and non-git directories are entirely unaffected.isolated— each worktree sees only its own task files. For worktrees that keep intentionally independent task states.
# .taskmd.yaml
worktree_scope: isolatedWhen the overlay is active:
- Read commands (
list,next,board,stats,get,graph, …) show each task's effective status — the most advanced status across all worktree copies — with provenance for the worktree that owns it. taskmd nextnever recommends a task that isin-progress(or further along) in a sibling worktree, so marking a taskin-progressin one worktree claims it for the whole repository.- Mutations (
set,add,rm,archive) always write to the current worktree's files;taskmd setfails with a guard error when the target task exists only in a sibling worktree. - All worktrees of one repository resolve to a single registered project — no duplicate entries or double-counting in
--all-projects.
Across projects. --all-projects reads merge each repository's worktrees too, so a task claimed in a sibling worktree shows the same effective status whether you ask from inside that repo or from a cross-project view. Each project's scope comes from its own .taskmd.yaml, so one repository can set worktree_scope: isolated while the others keep merging.
Override per invocation with the global --worktree-scope flag or the TASKMD_WORKTREE_SCOPE environment variable — unlike the config key, an explicit override applies to every project in the run. See the CLI guide for the full behavior reference.
Web Bind Address
web.host controls which network interface the dashboard listens on. The default, 127.0.0.1, accepts connections only from the machine running the server.
web:
host: 127.0.0.1 # this machine only (default)
port: 8080The dashboard's JSON API has no authentication, and unless read-only mode is on it serves PUT /api/tasks/{id}, which rewrites task markdown on disk. Widening the bind makes that reachable by anything that can route to the address, so treat --host as a deliberate choice rather than a convenience.
To reach the dashboard from another machine, bind every interface:
taskmd web start --host 0.0.0.0On a multi-homed host, bind the one interface you mean:
taskmd web start --host 10.0.0.1 --port 8380Whenever the bind is not loopback, the startup banner prints a warning naming what is exposed. Pair a wider bind with --readonly when you only need to view tasks remotely:
web:
host: 0.0.0.0
readonly: trueWARNING
taskmd does not authenticate web requests. On an untrusted network, restrict access at the host level (firewall, VPN, or a reverse proxy that adds auth) rather than relying on the bind address alone.
Usage Examples
Project Setup
# Create project config
cat > .taskmd.yaml <<EOF
dir: ./tasks
web:
port: 3000
auto_open_browser: true
EOF
# Now these commands use config defaults
taskmd list # Uses ./tasks directory
taskmd web start # Uses port 3000 and opens browser
# CLI flags still override config
taskmd list --task-dir ./other-tasks
taskmd web start --port 8080Global Defaults
Create ~/.taskmd.yaml for defaults used in directories that have no project .taskmd.yaml. It is a fallback, not a base layer: a project file replaces it entirely.
web:
port: 3000
auto_open_browser: trueEnvironment Variables
Every scalar config key can be set from the environment: prefix the key with TASKMD_, upper-case it, and replace . and - with _. So web.host becomes TASKMD_WEB_HOST and worktree_scope becomes TASKMD_WORKTREE_SCOPE:
export TASKMD_WEB_PORT=3000
export TASKMD_WEB_READONLY=true
export TASKMD_VERBOSE=trueCommonly used:
| Variable | Config key | Description |
|---|---|---|
TASKMD_TASK_DIR | task-dir | Task directory to scan (see the note below) |
TASKMD_WEB_HOST | web.host | Address the web server binds to (default 127.0.0.1; use 0.0.0.0 to accept connections from other hosts) |
TASKMD_WEB_PORT | web.port | Port the web server listens on (default 8080) |
TASKMD_WEB_READONLY | web.readonly | Start the web server read-only (the repo's docker-compose.yml sets this) |
TASKMD_WORKTREE_SCOPE | worktree_scope | unified or isolated |
TASKMD_VERBOSE, TASKMD_QUIET | verbose, quiet | Output verbosity |
Task directory from the environment
TASKMD_TASK_DIR only takes effect when the loaded .taskmd.yaml already has a task-dir key (likewise TASKMD_DIR needs a dir key). With no config file, or a config file without that key, the variable is ignored. This is a known limitation of the loader; use --task-dir / -d when you need an override that always works.
Docker
The published image already sets TASKMD_WEB_HOST=0.0.0.0 so that docker run -p 8080:8080 ... works out of the box — inside a container the network namespace is the isolation boundary and -p is the explicit opt-in. Override it if you want to restrict the bind further.
Precedence (highest to lowest):
- Command-line flags
- Environment variables (
TASKMD_*) - The one
.taskmd.yamlthat was found (nearest ancestor, else~/.taskmd.yaml), or the file given by--config - Built-in defaults
An environment variable overrides the config file. This is what makes docker run -e TASKMD_WEB_PORT=3000 work against an image whose mounted project already carries a .taskmd.yaml.
Sync Configuration
The sync command reads its configuration from the sync section of .taskmd.yaml. Each source defines where to fetch tasks from, how to map fields, and where to write files.
GitHub source:
# .taskmd.yaml
dir: ./tasks
sync:
sources:
- name: github
project: "owner/repo"
token_env: GITHUB_TOKEN # Environment variable holding the API token
output_dir: ./tasks/synced # Where to write synced task files
field_map:
status:
open: pending
closed: completed
priority:
urgent: critical
high: high
medium: medium
low: low
labels_to_tags: true # Convert issue labels to task tags
assignee_to_owner: true # Map assignee to owner field
filters:
state: open # Only sync open issuesJira source:
# .taskmd.yaml
dir: ./tasks
sync:
sources:
- name: jira
project: "PROJ" # Jira project key
base_url: https://myteam.atlassian.net # Jira Cloud instance URL (required)
token_env: JIRA_API_TOKEN # Jira API token
user_env: JIRA_USER_EMAIL # Jira account email (for Basic auth)
output_dir: ./tasks/jira
field_map:
status:
To Do: pending
In Progress: in-progress
Done: completed
priority:
Highest: critical
High: high
Medium: medium
Low: low
Lowest: low
labels_to_tags: true
assignee_to_owner: true
filters:
jql: 'status != "Done"' # Additional JQL (ANDed with project)Jira Authentication
Jira Cloud uses Basic authentication with your account email and an API token. Both token_env and user_env are required. Generate an API token at id.atlassian.net/manage-profile/security/api-tokens.
Jira Descriptions
Jira Cloud API v3 returns descriptions in Atlassian Document Format (ADF). taskmd automatically converts ADF to Markdown, supporting paragraphs, headings, lists, code blocks, blockquotes, and inline formatting.
Source fields:
| Field | Required | Description |
|---|---|---|
name | Yes | Unique name for this source |
project | No | Project identifier (e.g., owner/repo for GitHub) |
base_url | No | Custom API base URL |
token_env | No | Environment variable name for API token |
user_env | No | Environment variable name for username |
output_dir | Yes | Directory where synced task files are written |
field_map | No | How to map external fields to taskmd frontmatter |
filters | No | Source-specific filters (e.g., state: open) |
Field mapping (field_map):
| Sub-field | Type | Description |
|---|---|---|
status | map[string]string | Map external status values to taskmd statuses |
priority | map[string]string | Map external priority values to taskmd priorities |
labels_to_tags | bool | Convert external labels/categories to task tags |
assignee_to_owner | bool | Map external assignee to the owner field |
Phases Configuration
The phases key defines the phases available for your project. Tasks reference phases via the phase frontmatter field.
# .taskmd.yaml
phases:
- id: v0.2
name: "v0.2 – Core CLI"
description: "Core CLI features"
due: 2026-04-01
- id: v0.3
name: "v0.3 – Web Dashboard"
description: "Web dashboard"
due: 2026-06-01Each phase entry has the following fields:
| Field | Required | Description |
|---|---|---|
id | Recommended | Unique phase identifier (must match the task's phase field). If omitted, name is used as the id and validate warns. |
name | Yes | Human-readable display name for the phase. Missing name is a validation error. |
description | No | Description of the phase's scope |
due | No | Target date in YYYY-MM-DD format |
Behavior:
- When phases are configured, any
phasevalue in a task that does not match a configured phaseidproduces a warning. - When no phases config exists, all
phasevalues are accepted silently. - List order is significant.
nextrecommends earlier phases first: no task in a later phase is ranked above an actionable task in an earlier one, and score ranks tasks within a phase. A task with nophasecompetes in the earliest phase that still has actionable tasks; a task whose phase is not in the list ranks last. This applies to the CLI,next --all-projects(each project against its own list), the MCPnexttool, the web/api/nextendpoint, and the static export.taskmd next --strict-phases=falseopts out, treating phase as a score bonus only. Seenext.
Unphased tasks
next.unphased chooses where next ranks tasks that have no phase:
# .taskmd.yaml
next:
unphased: current # default — compete in the current phase on score
# unphased: last # rank after every configured phasecurrent(default) — an unphased task joins the earliest phase that still has actionable tasks, so an urgent unphased bug is not buried behind phased chores.last— unphased tasks rank after every actionable task in a configured phase (but still ahead of tasks whose phase is not in the list). Use it when "no phase" means "unscheduled" in your project.
Any other value is a config error, and so is a malformed next section (a plain value such as next: last, or an unknown key such as unphase): next (and the MCP server, web start and web export) refuses to run, and taskmd validate reports it. Like the phase list, the setting applies to every next surface, and next --all-projects reads it from each project's own .taskmd.yaml. It has no effect without phases or under --strict-phases=false.
Effort Configuration
The effort key defines the values the effort frontmatter field may take. It replaces the built-in small, medium, large entirely — it is not additive.
# .taskmd.yaml
effort: [xs, s, m, l, xl]Values are listed lowest to highest, and the order is significant:
| Behavior | How the order is used |
|---|---|
| Comparison filters | --filter "effort>=m" ranks values by position |
board --group-by effort | Column order follows the configured order |
list --sort effort | Sorts lowest first; unset values sort last |
next scoring | Points scale with position — the lowest value earns the most, the highest earns none |
next --quick-wins | Selects tasks at the lowest configured value |
Behavior:
- When no
effortconfig exists, the defaultsmall,medium,largeapplies and nothing changes. A bareeffort:with no value is treated the same way. - When configured,
taskmd validatereports any task using a value outside the list, andadd/setreject one. - The config itself is rejected when it is not a list, is empty, contains a blank entry, contains duplicates, or contains a non-string item.
- Items are effort values as strings. Object items are reserved for future per-value metadata.
Changing an existing project
The configured vocabulary replaces the default, so existing task files using small/medium/large become invalid. Update them in the same change, or keep the old values in the list.
Projects Configuration
The projects key in the global config registers projects for multi-project workflows. Use taskmd projects register to add entries, or edit the file directly.
The registry is read by its own loader from ~/.taskmd.yaml, or from the file named by $TASKMD_HOME_CONFIG when set. It is independent of the config discovery described above: --config does not change where the registry is read from, and projects and default_project are only meaningful in the global file. validate currently reports both keys as unknown; that is a known gap in the validator, not a sign the keys are ignored.
# ~/.taskmd.yaml
projects:
- id: frontend
name: "Frontend App"
path: /Users/me/projects/frontend
- id: backend
name: "Backend API"
path: /Users/me/projects/backend
# Optional: set a default project so --project can be omitted
default_project: frontendEach project entry has the following fields:
| Field | Required | Description |
|---|---|---|
id | Yes | Unique project identifier (auto-generated from directory basename if omitted during registration) |
name | No | Human-readable display name (defaults to the same value as id) |
path | Yes | Absolute path to the project directory. taskmd projects register refuses a directory with no .taskmd.yaml |
Usage:
# List all registered projects
taskmd projects
# Run commands against a specific project
taskmd list --project backend
taskmd next --project frontend
# Aggregate tasks from all projects
taskmd stats --all-projects
taskmd list --all-projectsdefault_project is the last fallback for the task directory: it applies only when no .taskmd.yaml is found from the current directory and no --task-dir was given. A discovered project config always wins, and --project or --all-projects override everything.
Shell Aliases
For quick access, add aliases to your shell config:
# ~/.bashrc or ~/.zshrc
alias tm='taskmd --task-dir ./tasks'
alias tmw='taskmd web start --port 8080 --open'
alias tnext='taskmd next --limit 3'
alias thigh='taskmd list --filter priority=high --filter status=pending'