Otto is a simple task runner. Think make, but with built-in retries, timeouts, run history, and notifications.
% otto run -- echo "hello"
ok run "inline" finished in 3ms
% otto history
inline ok success
source: inline
exit: 0
started (UTC): 2026-02-22 18:10:09
duration: 3msInstall from crates.io:
cargo install otto-cli --locked
otto versionUse exactly one command mode per task:
exec: direct argv execution (no shell parsing)run: shell command (/bin/sh -con macOS/Linux,cmd /Con Windows)tasks: compose other tasks by name
Task example:
tasks:
lint:
exec: ["cargo", "fmt", "--all", "--check"]
clippy:
exec: ["cargo", "clippy", "--all-targets", "--all-features", "--", "-D", "warnings"]
build:
exec: ["cargo", "build", "--release"]
ci:
tasks: ["lint", "build", "clippy"]
parallel: false # set true to run child tasks in parallelShared defaults live in defaults, and each task can override:
timeoutretries(0..10)retry_backoff(uses exponential backoff between attempts)notify_on(never,failure,always)
otto run auto-loads .env if present.
- Disable it:
--no-dotenv - Use a different file:
--env-file .env.staging
Variables from process env + dotenv + task env are expanded in:
runexecargsdirenvvalues
Unknown variables are preserved as ${NAME}.
Supported channels:
- desktop (
osascripton macOS,notify-sendon Linux) - webhook (
POSTJSON tonotifications.webhook_url)
notify_on controls when notifications fire: never, failure, always.
Every run gets recorded in .otto/history.jsonl.
For scripts, use:
otto run --jsonotto tasks --jsonotto history --jsonotto validate --json
In JSON mode, command output is suppressed so stdout is valid JSON only.
otto completion bash
otto completion zsh
otto completion fish
otto completion powershell