Command safety classifier
How every shell command gets parsed and classified by effect before it runs, and the absolute refusals nothing can override.
Every shell command ML.ai runs is lexed and parsed, not matched against a list of dangerous strings. Every position in the command where a program name could appear is walked, which means pipelines, subshells, and wrapper commands like sudo, xargs, env, and timeout are all inspected for the command they carry, not just the wrapper itself. Redirections are treated as writes regardless of which binary produced them: echo hello is harmless, echo hello > ~/.zshrc is a write to a file outside anything the agent should be touching unprompted.
Effect classes
Each command is classified into one of these effect categories; when a command mixes effects (a pipeline, a && chain), the worst verdict wins.
| Effect | Examples |
|---|---|
| Read | ls, cat, git status, grep |
| Verify | Test runners, typecheckers |
| Write inside workspace | Editing/creating a file under the project |
| Write outside workspace | Editing/creating a file elsewhere on disk |
| Network | Any outbound request |
| Irreversible deletion | rm -rf, force-pushes that discard history |
| History rewrite | git rebase, git commit --amend on shared history |
| System change | Editing system configuration |
| Privilege escalation | sudo and equivalents |
| Opaque execution (3 grades) | Commands whose actual behavior can't be inspected ahead of time, at increasing severity |
Each agent type holds its own budget defined over this same vocabulary: one shared effect table, many budgets. Adding a new recognized binary updates every agent's classification at once, rather than needing per-agent special-casing.
Worked example
git status && rm -rf ~This is judged on the rm -rf ~, not the harmless git status half. The refusal names the offending part specifically, rather than rejecting the whole line without saying why.
Reads never prompt
Read-only commands (ls, git status, grep) never trigger a prompt at all. This is what makes strict prompting everywhere else affordable: you aren't asked about the 95% of commands that only look at things.
Verification is trusted by name
Verification commands (test runners, typecheckers) run without a prompt even in Plan mode, because verification is what planning needs. This trust is by name: the classifier recognizes the spelling of npm test, it does not inspect what the underlying script actually does. A project-defined test script that happens to delete files would still be trusted at this layer.
Absolute refusals
These hold regardless of mode, agent, or setting, including auto-approve. Nothing overrides them:
- Deletion whose target is outside the workspace, or that can't be proven safe before running.
- Writes to devices, system roots, or the home directory.
sudoor any other privilege escalation.- Fork bombs.
- Reading stored credentials into a command that then reaches the network.
- Any attempt to modify the permission-rule store itself.
- Any command whose program can't be identified before running, for example
eval "$CMD", where the actual command isn't known until it's too late to classify.
Total by construction
The classifier cannot throw. Every failure path resolves to a refusal with a stated reason rather than an unhandled error. Its design invariant is asymmetric: it may err toward asking too often, never toward allowing something it shouldn't.
Every verdict, approved, prompted, or refused, is logged to the ML.ai output channel, regardless of outcome.
Turning it off
Add this to your VS Code settings:
{
"ml-ai.commandSafety": false
}With this off, every shell command prompts, regardless of how safe it actually is: a plain git status asks for approval just like a destructive delete would. There's no partial setting that trusts reads but disables the effect classification for everything else.
This restores prompt-on-every-shell-command behavior; it does not stop verdicts from being logged. Turning this off removes the by-effect classification, not the audit trail.