Docs / Developer mode
Shell commands
Every command an agent runs through run_shell is sorted into allow, ask or deny before it runs. This page covers how that verdict is reached, the full deny baseline, the two permissions files that tune it, and how the hosts a command names are judged.
run_shell
run_shell runs one command in the workspace. It is held by the agents with the shell.full grant: the main agent, the Code Engineer, the converters (the Report Writer, the Document and Deck Writer, the PDF Operator) and the GitHub Clerk. The strict profile drops it.
A command runs with a timeout of 120 seconds by default; timeout in a permissions file changes it, up to a ceiling of 600 that nothing raises. A command that runs past it, or whose call you cancel with Esc, is ended with everything it started. Long output is written whole to .ivralo/spill/, and the agent gets its head, its tail and where the rest is.
The command sees an allowlisted environment: PATH, the temp and home paths, the locale, the user name, TZ and the Windows essentials. No key, token or password reaches it, GH_TOKEN and GITHUB_TOKEN included; gh authenticates through your own gh auth login. Fixed switches such as GIT_TERMINAL_PROMPT=0 and GIT_EDITOR=true make a command that would open an editor or a prompt fail at once instead of hanging the turn.
The three verdicts
A command is split on its operators (&&, ||, |, ;, &), each part is classified, and the command is as bad as its worst part. The verdict is applied twice: in the tool itself and, in the session, before you are asked.
| Verdict | What | Examples |
|---|---|---|
| allow | Read-only lookups the policy can read whole, the read-only verbs of git and gh, and anything your allow list covers | ls, cat, head, grep, wc, which, git status, git log, git diff, gh pr list |
| ask | Anything else. In a session it opens the dialog; in a run nobody watches it is refused | uv run pytest, npm install, git commit, gh pr review |
| deny | The baseline below, and anything your deny list covers. Refused with a reason; nothing lifts it, not /confirm, not a, not an allow entry | rm -rf build, curl https://..., git push |
A read stays a read only when the policy can see everything in it. A $(...), a backtick or a $NAME outside single quotes makes a command ask, because the shell fills it in before the program starts. Installing a package is not read-only, so it asks like any other change to the project. ivralo calling itself (uv run ivralo ask) always asks.
git and gh are read twice: as written, and with global options, .exe and quotes stripped, so git -C sub push --force and "git" push --force are the same push. A git read with -c, --config-env or --exec-path, or with an option that runs a program or writes a file (--ext-diff, --output=), asks.
The deny baseline
| Class | Denied |
|---|---|
| Recursive delete | rm -r / rm --recursive, rmdir /s, rd /s, del with /s or /q, Remove-Item -Recurse or -Force, deleting by wildcard |
| Disk or system change | format, diskpart, bcdedit, shutdown, schtasks, sc, reg add/delete/import, net user, net localgroup, git config --global or --system (reads included) |
| Encoded command | powershell -e / -enc / -encodedcommand, certutil -decode, base64 -d |
| Nested shell | sh, bash, zsh, cmd, powershell, pwsh |
| Network | curl, wget, ssh, scp, sftp, ftp, telnet, nc, ncat, Invoke-WebRequest, Invoke-RestMethod, iwr, irm |
| System package manager | apt, apt-get, dnf, yum, brew, choco, winget, scoop |
| A push or a merge | every git push, git send-pack, git subtree push, gh pr merge, gh pr ready, gh pr review --approve, gh repo edit --enable-auto-merge, any gh api call naming a merge or ready, any gh api graphql call with a body |
| History rewrite | git reset --hard, git clean, git commit --amend, git rebase -i, git filter-branch, git filter-repo, git reflog expire, git gc --prune, git update-ref -d |
| Secrets | env, printenv, set of a key, token, secret or password, git credential, git config of a credential, gh auth token, gh auth logout, gh auth status --show-token, gh config get of a token, gh ssh-key, gh gpg-key |
| Hidden command | gh alias set, gh alias import, git config alias.* |
| Browser | gh browse, gh auth login, gh auth refresh |
| Irreversible | gh repo/issue/release/run/gist/project delete, gh release delete-asset, gh cache delete --all, gh api -X DELETE, a deleteRepository mutation |
| Closed paths | any word naming a .env or .env.* file, .ivralo/, .git/ or ivralo's own configuration files, in any spelling, a glob or a redirect included |
| Out of reach | a path that climbs out of the workspace, and a redirect that writes outside it |
The baseline reads what the program is handed, not what was typed, so a redirect in front (>x curl https://x) or quotes in a word (c"url") hide nothing. On Windows a command with a caret or a single quote is judged both as written and as cmd.exe will run it, and the stricter verdict wins.
The two permissions files
| File | Applies to |
|---|---|
permissions.json in the ivralo home (IVRALO_PERMISSIONS moves it) | Every workspace |
.ivralo/permissions.json in the repository | That repository alone; outside git, so it stays on your machine |
{
"version": 1,
"allow": ["uv run pytest", "libreoffice --headless"],
"deny": ["python -c"],
"timeout": 300,
"allow_tools": ["edit_file"],
"hosts": { "allow": ["pypi.internal.example.com"], "deny": ["tracker.example.net"] }
}
| Key | Meaning |
|---|---|
version | The file format, 1 |
allow | Command prefixes that run without asking |
deny | Command prefixes that are refused |
timeout | Seconds a command may run, capped at 600 |
allow_tools | File and git tools an unattended run may use (Editing and approvals) |
hosts | allow and deny lists of host names or addresses; a name covers its subdomains |
The two are merged: allow, deny, allow_tools and each hosts list are the union, so a deny in either file denies, and the baseline wins over an allow in either. timeout is the repository's when it sets one, otherwise the home's. ivralo doctor prints what each file contributes and names the file in front of every problem. Both files are read again at every pause, so a hand edit counts from the next call that would ask.
In a folder you have not trusted, the repository's file can only take permissions away: its deny and hosts.deny apply, and its allow, allow_tools, hosts.allow and timeout do not.
p: always allow this command
When the dialog asks about a single shell command, it offers p with the prefix it would store, such as [p] always "uv run pytest …" in this repository for uv run pytest -q tests/x.py. The prefix is the command's leading words up to the first option, path, quoted argument, redirect or pipe, three words at most. p runs the command and appends the prefix to the repository's .ivralo/permissions.json, never the home's; the next command with that prefix runs with an allowed line, with no restart.
There is no p when no prefix could keep its promise: the prefix would be one word cut from a longer command (python script.py would allow every python), a git command with an option that runs a program, a gh api call that changes something, a pipeline or a && chain, or a command the prefix still would not let run. When the offered prefix is wider than you mean, press Enter to run it once. In a folder you have not trusted, p is not offered.
/permissions what both files allow and deny
/permissions remove 2 take entry 2 out of this repository's file
Hosts and egress
A host the command names, in a URL or as user@host:path, is judged by the same egress policy that governs the web tools and the Python worker. The shell reads hosts from the string as written, without a DNS lookup.
| Host | Verdict |
|---|---|
| Link-local or cloud metadata address | Denied |
In hosts.deny | Denied |
In hosts.allow | Allowed |
| Loopback, a private address, a local name, or any host not listed | Asks, even when an allow entry covers the command |
An allow entry approves the command, not where it reaches. When a host is all that made a command ask, p stores the host in hosts.allow instead of a prefix: [p] always reach pypi.internal.example.com from this repository. A gh command that names no host is not judged for hosts, because gh reaches only the hosts it is logged into.