amont-agent 2.30.2

A guard that inspects a shell command before Claude Code runs it
# Shell analysis

`src/analysis/` answers one question about a single Bash command, before it
runs: which network transfers it may make, and which it must, with the
evidence for each count. `request-fanout` is its first consumer. The analysis
decides nothing; the rules do.

## Layers

| layer | module | job |
|---|---|---|
| frontend | `analysis::frontend` | source → crate-owned IR, spans on every node |
| semantics | `analysis::interp`, `state`, `domain` | abstract interpretation: state, control flow, exit statuses, counts |
| command models | `analysis::models` | a client's arguments → its explicit transfers |
| policy | `rules::request_fanout` | whether the result warrants advice, and the words |

Everything in `analysis` is pure. The dialect is an input; nothing reads the
environment, the filesystem or the network. The legacy lexer
(`src/shell.rs`) is untouched and still serves every other rule.

## Inputs

- **The command text.**
- **The dialect** — `bash`, `zsh` or `unknown`. The hook reads it from
  `SHELL`; `check --dialect` sets it; the backtester always uses `unknown`,
  because transcripts do not record the shell. Where bash and zsh differ, an
  `unknown` dialect gets the join of both readings — never one of them
  silently.

Dialect differences that are modelled: whether the last element of a
pipeline runs in the current shell (zsh yes; bash only under
`shopt -s lastpipe`); word-splitting of unquoted parameters (bash splits,
zsh does not); an unmatched glob in a URL-shaped word (bash passes it
through; zsh refuses the command); `break`/`continue` with no loop in the
current shell, as inside `( … )` (bash warns and carries on; zsh leaves).

Where the command does not say which `seq` runs, `seq 5 1` is either nothing
(GNU) or five numbers (BSD, macOS): both are allowed.

`tools/shell-oracle/` checks all of this against bash and zsh actually
running generated scripts with stubbed clients — see its `Cargo.toml`.

## Assumptions

Every "established" count holds under these, and the rule's message lists
them:

- the shell is not killed from outside;
- redirections succeed;
- `errexit` and `pipefail` are off unless the command sets them (then they are
  modelled);
- no aliases are defined;
- inherited environment variables are unknown;
- the command has no positional arguments of its own.

## What is counted

**Explicit command-line transfers**: the requests a client's arguments ask
for. Redirects, authentication hops, a client's own retries and pagination,
and defaults from `~/.curlrc` are named as possible extras and never counted.

A count is an interval: at least `lower`, at most `upper`, where `upper` is a
number, *saturated* (too large to represent), *uncapped* (positive evidence
that nothing bounds it: a loop following next-page links, `--paginate`,
`wget -r`, `while true` with no way out), or *unknown* (no evidence either
way). Zero annihilates: code that provably never runs makes no transfers,
whatever it would have done.

A finite upper bound on a loop comes only from the **counter pattern**: a
literal initial value, a comparison as the condition's final command, exactly
one unconditional `+1` of the counter in the body, and no other write to it —
including from a function the body calls, or from anything unmodelled — and
no `continue` that could skip the increment.

## Pacing

Each path through the interpreter carries how long it definitely slept
(`sleep 30`, `sleep 2m`; a backgrounded `sleep` does not count). A loop is
**paced** when every path back to its head — the end of the body, or a
`continue` — slept: a `continue` that skips the sleep, or a sleep behind a
condition, leaves it unpaced. A paced call site records the interval and its
**burst**: the transfers it makes per iteration, an inner loop or `--retry`
included. The innermost paced loop is the one recorded.

Paced and unpaced transfers never join across `if` branches: a poll in one
branch and a one-off request in the other are two different kinds of load.

`request-fanout` exempts a call site paced at least 10 seconds apart whose
burst is within its budget: a poll, not a burst, and `foreground-poll`'s
business if it runs in the foreground.

## The supported subset

Sequences, `&&`/`||`, pipelines and `!`, `if`/`elif`/`else`, `case` with `;;`,
`;&` and `;;&`, `for`, `for ((…))`, `while`, `until`, `{ }`, `( )`, `(( ))`,
`[[ ]]`, `[ ]`/`test`, function definitions in both forms, `local`/`typeset`,
`break`/`continue` with levels, `return`, `exit`, `set -e`,
`set -o pipefail`, `shopt -s lastpipe`; `$( )` and backticks, `$v`, `${v}`,
positional parameters, `$@`/`$*`, the `${v:-x}` family (not evaluated: its
value is unknown), every quoting form, unquoted brace expansion.

## Unknown and Incomplete

- **Unsupported** constructs — `eval`, `source`, `trap`, `xargs`,
  `parallel`, a command name built at run time, a program that runs another
  program — may do anything: every variable becomes unknown, the command may
  exit or never end, and the region is reported as activity the analysis
  could not follow. It never erases a known count elsewhere in the command.
- **Incomplete** means a resource limit ended the analysis (nesting depth,
  IR size, interpreter steps, call depth, recursion). What was found before
  it is kept; nothing after it is known.
- Cardinalities — `{1..999999999}`, `seq`, curl URL ranges — are computed
  arithmetically and never expanded.

A rule built on the analysis never fires on an unknown count alone.