Command rules

Command rule patterns

Command rule patterns for tenant admins and governance reviewers.

For: tenant admins and governance reviewers.

Patterns

A pattern names what a command runs, never its text. It is the program (its file name, after its wrappers: sudo rm -rf / is the part rm -rf /), then its flags and operands as the program reads them:

  • flags are read as the program does: -rf is -r and -f, --force and -f of git push are one flag, a shortened long option is the one it shortens, and -target=module.test.* is the flag -target with that value;
  • * is one operand, ** the rest of the command, and a word with * or ? in it is a glob over one operand (module.test.*);
  • [class] is an operand of a class: [home], [root], [inside-workdir], [outside-workdir], [unresolvable], [device], [default-branch], or a URL by its host, [host:get.k3s.io] ([host:*.example.com]);
  • a | b names a part b that reads a's output: a pipeline's earlier command, a substitution in its arguments or its input.

A program is named by its file name in lower case, with a Windows .exe, .cmd or .bat taken off: on macOS's and Windows' file systems RM and rm.exe run rm, so the reader and every rule read them as rm. A redirection is read as one, never as the program, including {name}>file, which opens a descriptor and assigns its number to name, so {fd}>/dev/null rm -rf ~ is read as rm -rf ~.

Allow matches

An Allow lets through what it writes and no more: the same flags (more only with **), the same number of operands, every value of a flag matching (-target=module.test.* does not allow a second -target elsewhere), and every part that feeds it named. So an Allow of one program never matches a part another program feeds: bash ** lets your scripts run, and still asks about curl ... | bash and bash <(curl ...); to allow a download, name it (curl -fsSL https://get.k3s.io | sh).

An Allow names a program and its arguments. It does not cover an environment variable supplied before it, through env or sudo, or through a client terminal's environment (FOO=1 git log, env FOO=1 git log).

It also excludes variables exported earlier in the command: export FOO=1, declare -x, set -a, set -k, or an assignment to an upper-case name such as PATH=... or a proxy setting. The shell's environment may already hold that name and pass it on. The exceptions only change how output is printed: LANG, LC_*, TZ, TERM, NO_COLOR, FORCE_COLOR, CLICOLOR, COLUMNS, LINES and CI.

An Allow does not cover a file the output goes to. This includes git log > notes.txt, a surrounding { ...; }, loop or if redirected that way, and an earlier exec > notes.txt that redirects every later command. /dev/null and another descriptor (2>&1) are allowed.

Such a command is decided by your other rules and the defaults as if no Allow matched.

Ask and Refuse matches

An Ask or a Refuse catches every part that holds what it writes, whatever else the part adds: rm -r [home] catches rm -rf ~ and sudo rm -r ./x ~.

Patterns that the shell expands

A pattern in the command (*, ? or [...] unquoted, where the reader reads one) is the names the shell makes of it, so a rule matches it by them rather than by its text. An Ask or a Refuse that names a file matches a pattern that may expand to it: a Refuse of cat secrets/prod.key catches cat secrets/prod.ke? and cat secrets/prod.[k]ey, a Refuse of rm a b catches rm *, and a Refuse of grep --include=*.key ** catches grep -r --include=*.ke? x ..

An Allow covers a pattern only where it covers every name the pattern may make: by a class (rm [inside-workdir] ** allows rm build/*.o) in the last operand it lists with a ** after it, since a pattern may be many operands, or for a flag's value by a literal start and a * (--include=src* allows --include=src*.go).

A * or a ? of an Allow stands for one word and one character, so rm x? and rm * do not allow rm x*.

A pattern quoted in the command (cat 'prod.ke?') is one name, and is matched as one.

An Ask or a Refuse matches a pattern in any case, since an account's startup file may set the shell to match names without regard to case: a Refuse of cat notes/todo.txt catches cat NOTES/todo.tx? too.

A class does the same: a path whose first folder is a pattern, or another case, that may make /dev is a [device] where the rest may name a disk, as well as what else it may name.

Variables in matching

A rule matches the words a program gets, a variable's value in them: with a variable the command set itself, d=old; rm -r $d is rm -r old to a rule of it, and s=status; git $s is the git status a rule names, for the defaults as for your rules.

A word whose text the reader cannot know (the command is then unreadable, and the floor asks) may be anything an Ask or a Refuse names, and is nothing an Allow names.

Paths in matching

A word of an Ask or a Refuse that spells a path is compared as a path, and an operand or a flag's value that spells one is read as the path it names, from where the command stands, through the same walk the classes go through.

A leading ./, a doubled /, a ., a .. through a folder, a cd into its folder, a fixed alias and an absolute path all name the same file as the rule's word does, a home folder's path is a path from ~, and a word the rule names as written stays named after a cd.

An absolute path is compared with the working directory when the call says which that is, part by part, so a pattern before it may be it: with /srv/proj as the working directory, cat /srv/p*/secrets/x is a path cat secrets/x names, and so is cat ~/proj/secrets/x where it stands in a home, as a word that climbs reaches the folder above.

A path that never climbed is read as inside the working directory, so it is compared with an absolute or a home word only when the call names that directory: with none, a Refuse of cat /etc/shadow does not name cat shadow.

Where the place a path stands in is not known (the call names no working directory, a cd to a variable, a path that climbed out of it), the two are compared as far as that place leaves open.

A word's wildcard may span folders, so the rest of the path is compared with it, never a count of parts: an absolute path may end in a word from the working directory, and a path that climbed may be how an absolute or a home word ends, or stand one or two parts above a home's: ../../home/kevin/.ssh/x and ../../root/.ssh/x may be ~/.ssh/x, and Refuses of cat secrets/** and cat /etc/* name cat ../p/secrets/a/b and cat ../../etc/ssl/x.

A path that climbed with nothing after it may be any folder such a word names, so a Refuse of rm -rf / names rm -rf ../...

A word with no parts of its own, . or .., is that folder: it names a path of no parts that climbed as far (./, a/.., /proc/self/cwd), and an absolute path only where the call says so.

A URL is no path. An

Allow stays with the words it writes, since a spelling that goes through a folder may leave it by a link.

Uncertain matches

A Refuse never turns into an Ask because the reader could not compare a word: what may match is refused.

That holds for a word whose text it cannot know, in an operand or a flag's value, whether the Refuse writes a word or a class there: rm -rf $X and cat /dev/$X may be what rm -rf [home] and cat [device] name, and a rule over a default narrowed to a class takes what that default found in such a word, as a host or a branch does unless the text the reader does know settles it (https://ok.example/$X is ok.example, and feature/$X is no default branch).

The same rule holds when an unknown word may match a Refuse in any of these ways:

  • It may be a flag the Refuse names.
  • It may expand to several words, each of which may be an operand the rule lists.
  • The pattern starts with the home folder.
  • ** may stand for any depth of folders.
  • A pattern under /dev has literal ends that agree with a disk's name, whether or not the reader knows such a disk.
  • Reading the word passes the reader's work bound. Comparing rules with commands counts toward that bound too, so a very long command under many rules for its program may be refused by the first rule it reaches.

What a Refuse cannot see is a part the reader never reached: a command it could not parse, or the program of one named by a variable, has no such word to compare, and the floor asks about it unless a Refuse over cmd.unreadable makes the floor refuse.

A wrapper never matches by itself: a rule for sudo *, env ..., xargs ... or sh -c * is refused when you save it, since the command it runs is read and decided on its own. So is a rule for eval. A pattern is at most 512 characters and 4 programs, and each of its words at most 256 bytes, since every word is matched on every command.

Copyright © 2026