/security-tiers
Use when classifying any operation before executing it, or deciding whether user approval is required
Claude Code
$ npx -y skills add metraton/gaia --skill security-tiers --agent claude-code
Codex
$ npx skills add metraton/gaia --skill security-tiers -a codex -y
Cursor
$ npx skills add metraton/gaia --skill security-tiers -a cursor -y
Gemini CLI
$ npx skills add metraton/gaia --skill security-tiers -a gemini-cli -y
GitHub Copilot
$ npx skills add metraton/gaia --skill security-tiers -a github-copilot -y
OpenCode
$ npx skills add metraton/gaia --skill security-tiers -a opencode -y
Windsurf
$ npx skills add metraton/gaia --skill security-tiers -a windsurf -y
SKILL.md
security-tiers.SKILL.mdname: security-tiers
description: Use when classifying any operation before executing it, or deciding whether user approval is required
Security Tiers
COMMAND_SET boundary
Classification applies to each exact atomic command. After read-only investigation, one or more predictable T3 commands may be requested plan-first with `gaia approvals request-set` -- a single predictable T3 command uses the same verb proactively, not only after the pre-execution policy gate returns `[T3_BLOCKED]`. Never discover a set by attempting a compound shell command. `subagent-request-approval` owns request construction and grouping; this skill owns the classification and no-elusion decision that routes work there.
security-tiers classifies every operation into four tiers so an agent knows whether it can run freely or must request the user's consent.
The validator model: a simple first line, walls behind it
The Bash classifier is the first line, and it is kept simple on purpose: the target walls below stand behind it, so a command it misreads still meets a wall at the thing that matters. It applies five rules; the sections after this one detail each lane.
1. **A closed change-verb lexicon.** A word changes state only when it is in `hooks/modules/security/verb_lexicon.py::CHANGE_VERB_CLASSES` -- five classes, `create`, `update`, `delete`, `publish` and `exec` -- whose union is `hooks/modules/security/mutative_verbs.py::MUTATIVE_VERBS`. A word outside it is not a change verb, so `add`, `commit` and `stash` are free by elimination. The ambiguous words `set`, `copy`, `start`, `tag`, `label` and `annotate` (`verb_lexicon.py::AMBIGUOUS_VERBS`) are relieved only on a family proven local, `LOCAL_FAMILIES` (docker), through `verb_lexicon.py::ambiguous_verb_relieved`; an unmodelled CLI (`rclone copy`) is not proven local and signs. Inside the execution scan (rule 3) a lexicon verb counts only on a remote family (`hooks/modules/security/execution_scan.py::REMOTE_FAMILIES`: k8s, iac, git, cloud, workspace, package, system). 2. **A closed exception table.** The verb yields to the effect only in the shapes `verb_lexicon.py::EFFECT_OVER_VERB_EXCEPTIONS` lists, each naming the symbol that decides it: a push of one's own non-default branch and a `--ff-only` merge in a managed worktree (`mutative_verbs.py::_check_git_publish_relief`), a local `git merge` (`mutative_verbs.py::_git_merge_is_local`), a `--ff-only` merge to the branch's own remote ref (`mutative_verbs.py::_git_local_sync`), opening or editing a change request (`verb_lexicon.py::CHANGE_REQUEST_OWN_WORK`), and `gaia approvals revoke|reject|reject-all|clean` (`mutative_verbs.py::CONSENT_REDUCING_SUBCOMMAND_EXCEPTIONS`). Opening a change request is free on any repository; a local merge is free with only the plain flags `mutative_verbs.py::_GIT_LOCAL_MERGE_FLAGS` lists (plus `-m <msg>`, or a bare `--abort`, `--continue`, `--quit`), while a strategy option (`-s`, `-X`) or an operand the shell computes (`git merge $BRANCH`) signs; `pr merge` signs for every binary, and so does a change-request edit on another repository or with a retarget flag. "What reaches the remote" below gives each shape in full. 3. **The word that decides what runs is literal.** When the program word, or the verb word of a CLI that picks its action by word, is a shell expansion, the command asks as `unknown.dynamic-span` (verb `dynamic-program` or `dynamic-verb`; Step 0-dynamic in `mutative_verbs.py::_detect_mutative_command`, reading `_literal_words` and `_dynamic_verb_span_word`). 4. **One short execution scan, depth 1.** `execution_scan.py::scan_execution` reads an execution's entry -- a script file, inline `-c`/`-e` code, a heredoc program, a package script with its `pre`/`post` scripts, a `make` target or `just` recipe with its direct prerequisites, `go run`, `cargo run` -- and the literal local files it names, once each and never their own callees (at most `MAX_CALLEES`, 32, each up to `MAX_BYTES`, 512 KiB). `execution_scan.py::_signal` looks for six signals: a change verb on a remote family (`remote-change-verb`, `sign.lands`); a write -- never a read -- onto a system, account or harness path (`system-path`, `sign.system-path` or `sign.lands-on-protected`); a download piped into an interpreter (`download-and-run`, `sign.foreign-code`); code built at runtime, such as `eval` of a variable or an `import` whose specifier is computed (`runtime-code`, `sign.foreign-code`); a recursive delete or truncate outside the project or Gaia scratch (`destroys-outside`, `sign.destroys`, measured against `execution_scan.py::_kept_roots`; a permanently blocked disk or delete command reports as `floor` under the same rule); data sent over the network (`network-send`, `sign.speaks`). The scan only adds asks (`mutative_verbs.py::_check_execution_scan`, run right after `_detect_mutative_command`): a verdict the older lanes already made mutative is kept unless its verb is one of `mutative_verbs.py::_NO_MODEL_VERBS` -- an unresolved `npm`/`bun`/`pnpm`/`yarn` run, an unresolved frozen install, an unreadable script file -- and only such an ask is freed when the scan reads the entry with no signal, never one whose entry is inline code. A file read only in part (over the size cap or not UTF-8) asks as `partial-read` (`unknown.dynamic-span`), and an entry that runs a local path the scan cannot read keeps the older verdict. 5. **Reads are free, except secrets and the store.** What changes nothing is free by elimination (`hooks/modules/security/tiers.py::_classify_command_tier_cached`). Reading where secrets live is refused (`hooks/modules/security/sensitive_read_guard.py::check`), and so is any `sqlite3` read of Gaia's store or Python that reaches it in-process (`hooks/modules/security/gaia_db_write_guard.py::check`, which calls `hooks/modules/security/store_code_guard.py::check`); `gaia context query` opens the store read-only.
The target walls behind those rules:
- **Running as another user signs
Read more
name: security-tiers description: Use when classifying any operation before executing it, or deciding whether user approval is required
Security Tiers
COMMAND_SET boundary
Classification applies to each exact atomic command. After read-only investigation, one or more predictable T3 commands may be requested plan-first with `gaia approvals request-set` -- a single predictable T3 command uses the same verb proactively, not only after the pre-execution policy gate returns `[T3_BLOCKED]`. Never discover a set by attempting a compound shell command. `subagent-request-approval` owns request construction and grouping; this skill owns the classification and no-elusion decision that routes work there.
security-tiers classifies every operation into four tiers so an agent knows whether it can run freely or must request the user's consent.
The validator model: a simple first line, walls behind it
The Bash classifier is the first line, and it is kept simple on purpose: the target walls below stand behind it, so a command it misreads still meets a wall at the thing that matters. It applies five rules; the sections after this one detail each lane.
1. **A closed change-verb lexicon.** A word changes state only when it is in `hooks/modules/security/verb_lexicon.py::CHANGE_VERB_CLASSES` -- five classes, `create`, `update`, `delete`, `publish` and `exec` -- whose union is `hooks/modules/security/mutative_verbs.py::MUTATIVE_VERBS`. A word outside it is not a change verb, so `add`, `commit` and `stash` are free by elimination. The ambiguous words `set`, `copy`, `start`, `tag`, `label` and `annotate` (`verb_lexicon.py::AMBIGUOUS_VERBS`) are relieved only on a family proven local, `LOCAL_FAMILIES` (docker), through `verb_lexicon.py::ambiguous_verb_relieved`; an unmodelled CLI (`rclone copy`) is not proven local and signs. Inside the execution scan (rule 3) a lexicon verb counts only on a remote family (`hooks/modules/security/execution_scan.py::REMOTE_FAMILIES`: k8s, iac, git, cloud, workspace, package, system). 2. **A closed exception table.** The verb yields to the effect only in the shapes `verb_lexicon.py::EFFECT_OVER_VERB_EXCEPTIONS` lists, each naming the symbol that decides it: a push of one's own non-default branch and a `--ff-only` merge in a managed worktree (`mutative_verbs.py::_check_git_publish_relief`), a local `git merge` (`mutative_verbs.py::_git_merge_is_local`), a `--ff-only` merge to the branch's own remote ref (`mutative_verbs.py::_git_local_sync`), opening or editing a change request (`verb_lexicon.py::CHANGE_REQUEST_OWN_WORK`), and `gaia approvals revoke|reject|reject-all|clean` (`mutative_verbs.py::CONSENT_REDUCING_SUBCOMMAND_EXCEPTIONS`). Opening a change request is free on any repository; a local merge is free with only the plain flags `mutative_verbs.py::_GIT_LOCAL_MERGE_FLAGS` lists (plus `-m <msg>`, or a bare `--abort`, `--continue`, `--quit`), while a strategy option (`-s`, `-X`) or an operand the shell computes (`git merge $BRANCH`) signs; `pr merge` signs for every binary, and so does a change-request edit on another repository or with a retarget flag. "What reaches the remote" below gives each shape in full. 3. **The word that decides what runs is literal.** When the program word, or the verb word of a CLI that picks its action by word, is a shell expansion, the command asks as `unknown.dynamic-span` (verb `dynamic-program` or `dynamic-verb`; Step 0-dynamic in `mutative_verbs.py::_detect_mutative_command`, reading `_literal_words` and `_dynamic_verb_span_word`). 4. **One short execution scan, depth 1.** `execution_scan.py::scan_execution` reads an execution's entry -- a script file, inline `-c`/`-e` code, a heredoc program, a package script with its `pre`/`post` scripts, a `make` target or `just` recipe with its direct prerequisites, `go run`, `cargo run` -- and the literal local files it names, once each and never their own callees (at most `MAX_CALLEES`, 32, each up to `MAX_BYTES`, 512 KiB). `execution_scan.py::_signal` looks for six signals: a change verb on a remote family (`remote-change-verb`, `sign.lands`); a write -- never a read -- onto a system, account or harness path (`system-path`, `sign.system-path` or `sign.lands-on-protected`); a download piped into an interpreter (`download-and-run`, `sign.foreign-code`); code built at runtime, such as `eval` of a variable or an `import` whose specifier is computed (`runtime-code`, `sign.foreign-code`); a recursive delete or truncate outside the project or Gaia scratch (`destroys-outside`, `sign.destroys`, measured against `execution_scan.py::_kept_roots`; a permanently blocked disk or delete command reports as `floor` under the same rule); data sent over the network (`network-send`, `sign.speaks`). The scan only adds asks (`mutative_verbs.py::_check_execution_scan`, run right after `_detect_mutative_command`): a verdict the older lanes already made mutative is kept unless its verb is one of `mutative_verbs.py::_NO_MODEL_VERBS` -- an unresolved `npm`/`bun`/`pnpm`/`yarn` run, an unresolved frozen install, an unreadable script file -- and only such an ask is freed when the scan reads the entry with no signal, never one whose entry is inline code. A file read only in part (over the size cap or not UTF-8) asks as `partial-read` (`unknown.dynamic-span`), and an entry that runs a local path the scan cannot read keeps the older verdict. 5. **Reads are free, except secrets and the store.** What changes nothing is free by elimination (`hooks/modules/security/tiers.py::_classify_command_tier_cached`). Reading where secrets live is refused (`hooks/modules/security/sensitive_read_guard.py::check`), and so is any `sqlite3` read of Gaia's store or Python that reaches it in-process (`hooks/modules/security/gaia_db_write_guard.py::check`, which calls `hooks/modules/security/store_code_guard.py::check`); `gaia context query` opens the store read-only.
The target walls behind those rules:
- **Running as another user signs
Generative AI Interface for Agents. Specialist agents for Claude Code and OpenCode, with a memory that outlives the session and a consent gate on everything that changes state.
Other skills on gaia.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about security-tiers.
- Which coding agents can use security-tiers?
- security-tiers installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

