Skip to content

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:

yaml
# .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: false

Config File Locations ​

taskmd loads exactly one config file. It searches, in order:

  1. The current directory, then each parent directory up to the repository root (the first directory containing .git)
  2. ~/.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 ​

OptionTypeDefaultDescription
dirstring.Default task directory
ignorestring[][]Additional directories to skip when scanning (beyond the built-in skip list)
worklogsbooleanfalseAllow writing worklog entries; set to true to opt in
workflowstring"solo"Workflow mode: "solo" or "pr-review"
worktree_scopestring"unified"unified merges task state across git worktrees; isolated keeps each worktree to its own files (details)
todos.excludestring[][]Glob patterns to exclude from TODO/FIXME scanning
web.portinteger8080Web server port
web.hoststring127.0.0.1Web server bind address; 0.0.0.0 binds every interface (details)
web.auto_open_browserbooleanfalseAuto-open browser on web start
web.readonlybooleanfalseStart web server in read-only mode (disables editing)
id.strategystring"sequential"ID generation strategy (details)
id.prefixstring""Prefix for prefixed strategy
id.lengthinteger6Length of generated IDs (used by random and ulid strategies)
id.paddinginteger3Zero-padding width for sequential strategy
effortstring[][small, medium, large]Ordered effort vocabulary, lowest to highest (details)
scopesmap—Scope-to-path mappings for the touches field (details)
phasesarray[]Phase definitions with metadata (details)
next.unphasedstring"current"Where next ranks tasks with no phase: current (the current phase) or last (after every phase) (details)
projectsarray[]Registered projects for multi-project workflows (details) (global config only)
default_projectstring""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:

yaml
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 workflows
  • pr-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:

yaml
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:

StrategyFormatExample IDDescription
sequentialZero-padded number001, 042Default. Finds the highest existing ID and increments.
prefixedPrefix + numberdr-001, jk-042Like sequential, but with a user-defined prefix. Useful for multi-contributor projects.
randomRandom alphanumerica3f9x2Short random string. Avoids ID collisions across branches.
ulidULID01HGW3...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):

yaml
id:
  strategy: sequential
  padding: 3            # 001, 002, ... 999

Prefixed (useful for teams):

yaml
id:
  strategy: prefixed
  prefix: "dr-"         # used literally: dr-001, dr-002, ...

Random (branch-safe):

yaml
id:
  strategy: random
  length: 6             # optional, default 6

ULID (sortable and unique):

yaml
id:
  strategy: ulid
  length: 10            # optional, default 6

Avoiding 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.

yaml
# .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:

FieldRequiredDescription
descriptionNoHuman-readable explanation of what the scope covers. Included in validation error messages.
pathsNoList of file or directory paths that the scope maps to.

Behavior:

  • When scopes are configured, any touches value in a task that does not match a configured scope produces a warning.
  • When no scopes config exists, all touches values 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.
yaml
# .taskmd.yaml
worktree_scope: isolated

When 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 next never recommends a task that is in-progress (or further along) in a sibling worktree, so marking a task in-progress in one worktree claims it for the whole repository.
  • Mutations (set, add, rm, archive) always write to the current worktree's files; taskmd set fails 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.

yaml
web:
  host: 127.0.0.1   # this machine only (default)
  port: 8080

The 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:

bash
taskmd web start --host 0.0.0.0

On a multi-homed host, bind the one interface you mean:

bash
taskmd web start --host 10.0.0.1 --port 8380

Whenever 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:

yaml
web:
  host: 0.0.0.0
  readonly: true

WARNING

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 ​

bash
# 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 8080

Global 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.

yaml
web:
  port: 3000
  auto_open_browser: true

Environment 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:

bash
export TASKMD_WEB_PORT=3000
export TASKMD_WEB_READONLY=true
export TASKMD_VERBOSE=true

Commonly used:

VariableConfig keyDescription
TASKMD_TASK_DIRtask-dirTask directory to scan (see the note below)
TASKMD_WEB_HOSTweb.hostAddress the web server binds to (default 127.0.0.1; use 0.0.0.0 to accept connections from other hosts)
TASKMD_WEB_PORTweb.portPort the web server listens on (default 8080)
TASKMD_WEB_READONLYweb.readonlyStart the web server read-only (the repo's docker-compose.yml sets this)
TASKMD_WORKTREE_SCOPEworktree_scopeunified or isolated
TASKMD_VERBOSE, TASKMD_QUIETverbose, quietOutput 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):

  1. Command-line flags
  2. Environment variables (TASKMD_*)
  3. The one .taskmd.yaml that was found (nearest ancestor, else ~/.taskmd.yaml), or the file given by --config
  4. 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:

yaml
# .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 issues

Jira source:

yaml
# .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:

FieldRequiredDescription
nameYesUnique name for this source
projectNoProject identifier (e.g., owner/repo for GitHub)
base_urlNoCustom API base URL
token_envNoEnvironment variable name for API token
user_envNoEnvironment variable name for username
output_dirYesDirectory where synced task files are written
field_mapNoHow to map external fields to taskmd frontmatter
filtersNoSource-specific filters (e.g., state: open)

Field mapping (field_map):

Sub-fieldTypeDescription
statusmap[string]stringMap external status values to taskmd statuses
prioritymap[string]stringMap external priority values to taskmd priorities
labels_to_tagsboolConvert external labels/categories to task tags
assignee_to_ownerboolMap 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.

yaml
# .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-01

Each phase entry has the following fields:

FieldRequiredDescription
idRecommendedUnique phase identifier (must match the task's phase field). If omitted, name is used as the id and validate warns.
nameYesHuman-readable display name for the phase. Missing name is a validation error.
descriptionNoDescription of the phase's scope
dueNoTarget date in YYYY-MM-DD format

Behavior:

  • When phases are configured, any phase value in a task that does not match a configured phase id produces a warning.
  • When no phases config exists, all phase values are accepted silently.
  • List order is significant. next recommends 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 no phase competes 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 MCP next tool, the web /api/next endpoint, and the static export. taskmd next --strict-phases=false opts out, treating phase as a score bonus only. See next.

Unphased tasks ​

next.unphased chooses where next ranks tasks that have no phase:

yaml
# .taskmd.yaml
next:
  unphased: current   # default — compete in the current phase on score
  # unphased: last    # rank after every configured phase
  • current (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.

yaml
# .taskmd.yaml
effort: [xs, s, m, l, xl]

Values are listed lowest to highest, and the order is significant:

BehaviorHow the order is used
Comparison filters--filter "effort>=m" ranks values by position
board --group-by effortColumn order follows the configured order
list --sort effortSorts lowest first; unset values sort last
next scoringPoints scale with position — the lowest value earns the most, the highest earns none
next --quick-winsSelects tasks at the lowest configured value

Behavior:

  • When no effort config exists, the default small, medium, large applies and nothing changes. A bare effort: with no value is treated the same way.
  • When configured, taskmd validate reports any task using a value outside the list, and add/set reject 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.

yaml
# ~/.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: frontend

Each project entry has the following fields:

FieldRequiredDescription
idYesUnique project identifier (auto-generated from directory basename if omitted during registration)
nameNoHuman-readable display name (defaults to the same value as id)
pathYesAbsolute path to the project directory. taskmd projects register refuses a directory with no .taskmd.yaml

Usage:

bash
# 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-projects

default_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:

bash
# ~/.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'

Released under the MIT License. v0.8.0