ML.ai CodeControl & Safety

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.

EffectExamples
Readls, cat, git status, grep
VerifyTest runners, typecheckers
Write inside workspaceEditing/creating a file under the project
Write outside workspaceEditing/creating a file elsewhere on disk
NetworkAny outbound request
Irreversible deletionrm -rf, force-pushes that discard history
History rewritegit rebase, git commit --amend on shared history
System changeEditing system configuration
Privilege escalationsudo 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.
  • sudo or 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.