Skip to content

Configuration

Settings are discovered the way tasks are: from files along the path to your current directory, nearer files winning, with sensible behaviour when no config exists at all. Everything on this page is optional.

The precedence ladder

From weakest to strongest, each rung overriding the ones below it, key by key:

  1. Built-in defaults. No config, full behaviour.
  2. Your user-level file — ~/.config/footman/config.toml (honouring XDG_CONFIG_HOME; point FOOTMAN_CONFIG at a different file to move it). Personal defaults for every project: a purist's uv = false, a permanent progress = false.
  3. The project cascade — walking from the repo root down to your current directory, each directory may contribute settings; nearer directories override farther ones. Within one directory, a standalone footman.toml overrides [tool.footman] in pyproject.toml, the customary dedicated-file-wins rule.
  4. --config=PATH — total control over config: the named file replaces the user file and the config cascade entirely (the tasks side has the same escape hatch, -f/--tasks-file). You said exactly what applies.
  5. Environment variables — FOOTMAN_NO_UV, FOOTMAN_CACHE_DIR, and friends always beat file config. (NO_COLOR and FORCE_COLOR are gentler: they are --color's default, and a project's color key or an explicit --color= outranks them, because they speak for the terminal in general, not for this invocation.)
  6. Command-line flags — -s, -j, --no-progress… always win.

The cascade is what makes monorepos comfortable: a package deep in the tree can carry a two-line footman.toml that adjusts behaviour for that subtree only:

# services/deep/package/footman.toml — this subtree runs inside the
# already-active parent environment; don't hand off to uv run.
uv = false

The repo root's pyproject.toml still sets the shared defaults.

The files

In a pyproject.toml, settings live under the tool table:

[tool.footman]
tasks = "tasks.py"
sequential = false

A standalone footman.toml is the same keys, top-level:

tasks = "tasks.py"
sequential = false

The user-level ~/.config/footman/config.toml uses the standalone form. Unknown keys are kept but ignored, so a newer setting never breaks an older footman.

Where footman writes

Nothing footman writes lands inside your project. Three user-level directories, each honouring its XDG_* variable:

Directory Default What lives there
Cache ~/.cache/footman/ (XDG_CACHE_HOME) Derived data, safe to delete: completion manifests, timing history, fetch() downloads. The cache collector sweeps this directory and nothing else.
Data ~/.local/share/footman/ (XDG_DATA_HOME) Durable machine-local state that survives every cache sweep.
Config ~/.config/footman/ (XDG_CONFIG_HOME) Your own writing: the user-level config.toml and the user-level tasks file, which travel together.

The completion hooks are the one thing outside all three: they live at <data home>/<command>/completion.* — ~/.local/share/fm/completion.zsh, not ~/.local/share/footman/ — because they are keyed by the command you type rather than the config name. fm --uninstall-completion and fm self.uninstall both remove them.

What footman reserves

A plugin is welcome in these directories — footman.data_dir() is there so you do not have to invent a home of your own — so here is exactly what the runner claims, and what is therefore yours:

Directory footman writes free for you
cache_dir() <key>.json and <key>.times.json (completion manifests and timing history), global-<key>.json, gc.stamp, and the fetch/ folder any other name — but the collector sweeps this directory by age, so nothing you would mind losing
data_dir() builtins-<key>.json (the discovered built-in list, one per Python environment) any other name
config_dir() nothing — footman only ever reads config.toml and the tasks file here your own files, though footman will not read them

The safe convention is one subfolder named after your distribution — data_dir() / "acme-devkit" / … — which cannot collide with a future reserved name, since anything footman adds will be a file or folder named for what it does.

Two things to know before you settle in. cache_dir() is swept by age, so it is for things you can rebuild and nothing else. And fm self.uninstall removes the cache and data directories whole, plugin state included — the price of living in the runner's home rather than your own.

The spellings are the same on every operating system — the convention uv, ruff, and git's own XDG support follow — so on Windows everything lives under %USERPROFILE%: C:\Users\you\.cache\footman\, C:\Users\you\.config\footman\, and so on. The XDG_* variables move the base directories on any platform, and the footman-specific variables below move each corner on its own.

Tasks outside every project

Some tasks live outside any one project: creating one, cloning a repo, logging in. A package can ship them as a footman.tasks entry point, and the user-level builtin key mounts them as built-in tasks — the cascade's outermost rung, under your own tasks file and under every project's, so they are reachable wherever you stand and anything nearer shadows them by name. What each one offers where is the task's own answer (expose=), not an accident of the rung.

# ~/.config/footman/config.toml
[builtins]
user = ["acme_devkit"]      # entry points you name yourself

There are three sources, and they mount in this order: the runner's own set (declared in its code — the product), then whatever fm self.* discovered when you last added a package, then the names you wrote in builtins.user. Duplicates are dropped, so a name the runner already declares is never mounted twice.

The discovered list is machine-written and lives in footman's data directory, not here — so this file stays yours alone and nothing footman writes can trample a comment you left. builtins.user is the opposite: only you write it, footman only reads it, and a name in it that is not installed is refused rather than skipped.

builtins.discovery_mode says which of the three contribute:

mode the runner's own discovered user
auto (default) yes yes, kept current by fm self.* yes
manual yes yes, left exactly as it is yes
internal yes ignored yes
none no no yes

none is not an off switch — it means nothing automatic, only exactly what I named. It drops the runner's own commands too, including fm self.*, so if you want them back name them:

[builtins]
discovery_mode = "none"
user = ["footman.self"]

The whole table is user-level only: which packages your runner carries is your machine's business, not any project's, and a project that wants a set placed somewhere of its own mounts it the ordinary way in its tasks file.

A package's tasks belong to a project unless one says otherwise: @task(expose="always") says a task works anywhere, @task(expose="global_only") says it only makes sense before a project exists, and an unmarked task refuses by name where it does not belong rather than going missing. fm --plugins shows which source mounted what — built in for the runner's own, built in (your config) for the rest.

Editing the table reaches Tab on the press after next, the way every user-level edit does (see keeping the cache current).

Keys

Every key the runner recognises, rendered from its own list on each docs build, so this table can neither invent a key nor miss one:

key values default meaning
tasks filename tasks.py Task file to look for in each directory of the cascade.
sequential true / false false Run tasks one at a time by default; -s does it for one invocation.
jobs integer cores - 1 Max parallel tasks, never below 2. -j=N overrides it.
color always / never / auto auto When to emit ANSI colour, for footman's own output and the tools it spawns. --color/--no-color override it.
cwd policy token / absolute path taskfile Where tasks run by default: taskfile (the directory of the file that defined the task), root (the cascade's top), asinvoked (where you typed the command), unmanaged (footman holds no opinion), or an absolute path. A relative suffix belongs on a task's rel=.
sort true / false false List tasks alphabetically in --list, --tree, help and the generated docs pages. Default: definition order, so the file's own order is the listing's. Presentation only — never changes what runs.
progress true / false true false disables the progress bar, the eta line, and timing capture; --progress turns them back on for one invocation.
input true / false true false makes this project never prompt: a confirm() gate fails and an ask() without a default errors, rather than waiting for someone. --input allows prompting for one invocation, --no-input refuses it for one.
uv true / false true false disables both uv handoffs: re-running through the project's pinned footman, and the script environment of a tasks file carrying its own PEP 723 dependencies.
builtins.discovery_mode auto / manual / internal / none auto How the built-in set is assembled: auto lets fm self.* keep discovered list current, manual leaves that list to you, internal ignores it, none drops the runner's own set too. builtins.user is honoured in every mode. User-level only.
builtins.user list of entry points empty footman.tasks entry points you name, mounted as built-in tasks — the cascade's outermost rung. Nothing else writes this list, and a name that is not installed is refused. User-level only.
docs_url URL template unset Link task names in --list/--tree/--help to your generated task docs (and put a docs_url field on --json rows): {path} is the slash-joined task address, {slug} the dash-joined one. Terminal links ride the colour switch; piped output stays plain.
notes table of levels per kind Reclassify note kinds: [tool.footman.notes] maps [task/]kind patterns (either side *, most specific wins) to trace / info / warning / error — error fails the task at its boundary. See the notes page.
completion.max_age duration / off 10m Age before a background completion refresh (e.g. "10m"; off to disable).
fetch.backend urllib / curl / httpx / requests / auto urllib Download engine for fetch().
shell.default posix / native / pwsh / a shell name posix What run(shell=True) resolves to. posix is bash, then sh — git bash on Windows.
gc true / false true false disables the cache collector (it runs at most daily). User-level only — ignored in a project config, with a note under -v.
cascade none / repo / filesystem repo How far discovery ranges for task files and config: this directory only, the repository, or across repositories. User-level only; FOOTMAN_CASCADE overrides it per invocation.

Environment variables

Variable Effect
FOOTMAN_CONFIG Path of the user-level config file.
FOOTMAN_CONFIG_DIR Moves footman's config corner whole (the config file and the user-level tasks file travel together).
FOOTMAN_CACHE_DIR Moves every footman cache (completion manifests, timing history).
FOOTMAN_DATA_DIR Moves footman's data directory (durable files that survive a cache sweep).
FOOTMAN_NO_UV Disables both uv handoffs (project and script environment), regardless of any config.
FOOTMAN_NO_GC Disables the cache collector, regardless of any config.
FOOTMAN_CASCADE Overrides the cascade key for one invocation (none / repo / filesystem).
FOOTMAN_STACKS_AFTER Seconds between stack dumps to stderr, for a run that stops moving (Troubleshooting).
NO_COLOR / TERM=dumb Disable ANSI styling, for footman and for every tool it spawns, which footman tells to stay monochrome too.
FORCE_COLOR Force ANSI styling on, even piped (below --color and [tool.footman] color in the ladder).

Values for a task's environment are a different lane: .env files load through the first-party footman.env_files plugin (--env-file=.env) — see the built-in on the hooks page — and a per-parameter env("VAR") marker reads a single variable as a default.

See also Monorepos & config for how the tasks cascade itself composes, and the CLI reference for the flags that sit at the top of the ladder.