Docs/Reference

Reference

Shell commands: pre-bash and block-inline-execution

Two gates read every shell command an agent runs. tama hook block-inline-execution refuses a program written inline instead of in a checked-in file; tama hook pre-bash refuses throwaway work and commands that cross a product boundary. Neither decides by a list of program names: an interpreter, wrapper or cloud CLI nobody listed walked past every list that did.

Two gates read every shell command an agent runs. tama hook block-inline-execution refuses a program written inline instead of in a checked-in file; tama hook pre-bash refuses throwaway work and commands that cross a product boundary. Neither decides by a list of program names: an interpreter, wrapper or cloud CLI nobody listed walked past every list that did.

#What is read from structure

Identity and shape stay structural, because they are facts a command carries:

  • the tool itself: the evaluator, notebook and REPL tools, a browser run, a debugger evaluate, and a write to one of their devices are inline execution by what they are;
  • Weles' own entry points (its keeper, trajectory folder, session start), the Supabase program, the LSI/PARP/NCBR endpoints, the Overleaf history API, and the OpenAI and Anthropic key and host names;
  • a browser's cookie store by its own file name, and a raw ssh to a host the Stado registry declares;
  • writes into ~/.shared-hooks, git checkout, patch and interactive Git modes, a tool's co-author trailer, and pull-request work against a wisent-supabase-* repository.
  • an ssh, scp or rsync line that reaches an installed package (/site-packages/) or a systemd unit directory without a # justify: <reason> marker, whatever program writes there and whatever kind of file it is (BLOCKED: unsubstantiated constant injection detected; journey pre_bash_remote_constants).

Hook bypass on commit and push (--no-verify, commit -n) is block-git-hook-bypass's, which runs beside pre-bash on every shell call.

#Another session's uncommitted work

Every repository has one checkout and several sessions edit it at once. A Git command that discards what stands in the working tree, git restore <path> (without --staged alone), git reset --hard and git clean, is read against this session's own transcript, which says which files the session wrote (every write and file-change tool call). Every other uncommitted change the tree holds is another session's or the operator's, and a command that would discard one is refused naming the files: BLOCKED: `git restore src/lib.rs` would discard uncommitted changes this session did not make in <checkout>: src/lib.rs. Another session or the operator holds them in the one shared checkout. Commit only your own changes (`tama stage --session <session> --repo <checkout>`, then `git commit -m <message>` with no paths) and leave theirs; what you must undo in a file you did not write is theirs to undo. Discarding only one's own files passes; unstaging passes, because the change stays in the tree. The changed paths are Git's own (git diff --name-only HEAD and the untracked files); each command is judged against what it takes — a restore or a reset --hard against the tracked changes and untracked files under its paths (the whole tree for a bare reset), a clean against the untracked files alone, with the files the ignore rules name when it carries -x and only those with -X (a wholly ignored folder named once, build/), as Git's own ls-files --others --ignored --exclude-standard --directory lists them under the clean's paths — and the command's -C <dir> names the checkout judged; a path of . names the whole tree. git stash drop and git stash clear are refused while the stash holds any entry, naming the entries: Git records no session on a stash entry, so whose it is cannot be told (BLOCKED: `git stash drop` would delete stash entries of <checkout>: stash@{0}: On main: … Git records no session on a stash entry, so whose they are cannot be told; leave them. …). A discard whose ownership cannot be read — no session transcript in the payload, a transcript that cannot be read, a checkout Git cannot list — is refused with what could not be read, because passed it threw away whatever another session held: BLOCKED: whether `git restore src/lib.rs` would discard uncommitted changes another session holds in <checkout> cannot be told: the session transcript <path> cannot be read. Leave the tree as it stands and commit only your own changes: `tama stage --session <session> --repo <checkout>`, then `git commit -m <message>` with no paths. A written path that cannot be resolved for a cause other than its removal is refused the same way, naming the path. The journey is tests/shared-checkout/discard.rs.

Commit ownership is checked at hunk content, not path membership. tama stage --session reconstructs successful before/after edit receipts and stages only their deltas. The commit guard uses that same reader to compare the complete index with the session's recorded changes. A session editing one part of a file does not authorize another session's staged changes elsewhere in it.

Commit the inspected index in a separate invocation. Whole-path commits, --all, --include, pathspec-file commits and compound shell commands refuse before they can import uninspected working-tree changes. Git's own command declaration identifies option meanings. Missing transcripts, receipts, conflicting deltas or unreadable index content refuse with the actual cause. No compiler runs. --dry-run commits nothing.

The real journey tests/shared-checkout/commit.rs stages recorded edits through the public CLI against actual captured session receipts, compares the exact resulting Git tree, repeats the operation, verifies overlap refusal and checks that the operator's index and working files remain unchanged. It requires a retained session-edit capture; a missing capture is not a passing result.

#Output that never reaches the agent

A failed or hung step read as quiet success when its output was thrown away on the way. Two shapes are refused. A redirection into the null device, by the shell's own syntax (>/dev/null, 2>/dev/null, 2>>/dev/null, &>/dev/null, >&/dev/null), is read from structure and refused naming the construct: BLOCKED: `2>/dev/null` sends a stream of the command into the null device, so what the command says there (its errors included) never reaches you, and a failed or hung step reads as quiet success. Run the command with its full output and read all of it …. The redirection is read from the executable text alone: a program handed /dev/null as an argument (curl -o /dev/null) and a commit message or quoted sentence that mentions 2>/dev/null send nothing anywhere and pass. A filter that keeps only part of the producing command's output (its first or last lines or bytes, a column range of every line, a count instead of the lines) is the judge's shape TRUNCATE, answered from what the command does and not from the filter's name; counting matches, picking a field of a record, selecting matching lines and formatting a value are not truncation. The refusal: BLOCKED: the command keeps only part of what it says … Run the producing command with its full output and read all of it; a long output is retained whole by the harness, and a wait that does not end is read from the open-wait register. A question the judge leaves unanswered refuses the command, as for every judged shape. The journey is tests/hidden-output/null_sink.rs; its truncation case asks the real judge under TAMA_TEST_PRE_BASH_JUDGE=1 and -- --ignored.

#Which text in a command is code

Every shell gate reads a command through one shared reader that keeps shell syntax and drops literal arguments. A command line is code when a shell is handed it: after the shell's -c flag (bundled letters such as -lc included), through a wrapper such as env or sudo, or as a here-document the shell consumes. Which program is a shell is the system's own declaration, /etc/shells; a shell added there is read as one without a Tama change. When /etc/shells cannot be read every program is read as a shell and stderr says shell reading: /etc/shells cannot be read: …. What any other interpreter is handed (python3 -c, a here-document into Node) is data to these readers, because it is an inline program and block-inline-execution refuses it. The journey cargo test --manifest-path rust/Cargo.toml -p tama-cli --test shell_reading_system_shells hides a cloud provider command in each shell shape and requires block-cloud-instances to refuse it, and to pass the same line handed to python3 or written in a commit message.

#What a model reads

What a command does is read by a model through Oko's model call, and only after every structural rule passed it. block-inline-execution asks only when the command's syntax can carry source at all (quoting, a here-document, a process substitution, /dev/stdin, a command list or group, eval), and is answered whether an interpreter's execute flag, a filter-language program, shell control flow or fed source runs there. pre-bash asks with the command and its working directory, and the model names the first forbidden shape it does: a write into to_check, a scratch script under /tmp, a source file written through the shell, a keyword search over code, a browser engine started directly, a program run out of a Weles checkout, a cloud/cluster/IaC/hosting CLI, a file under DO_NOT_RUN_quarantine_* executed, a local direct-API helper, or a script run out of the hook tree (which passes under HOOK_EDIT_AUTHORIZED=1 in settings). The refusal is the operator's sentence for that shape.

A question the judge leaves unanswered refuses the command with the judge's recorded state, as asking a model describes; a line whose every command runs a program the judge stands on (Tama, Oko and the products Oko depends on) passes so the judge can be repaired:

TAMA_HOOK_INFRASTRUCTURE: block-inline-execution: review unavailable: …
TAMA_HOOK_INFRASTRUCTURE: pre-bash: review unavailable: …

A judge answer naming a shape it was not asked for passes with pre-bash: the judge answered a shape it was not asked for (…); the command passes.

#Trajectory runs

pre-bash counts a trajectory run when the command names the trajectory path or a script whose own source starts a Weles session, whatever program the script is handed to. The counters live in ~/.shared-hooks/state/trajectory_runs.json; post-trajectory-capture reads the same script the same way.

#Where it comes from

Source: rust/crates/tama-hook-tools/src/ports/block_inline_execution/ and rust/crates/tama-hook-tools/src/ports/pre_bash/ (prompt: vocabulary/throwaway-prompt.md). Journeys, run against the built hook with the judge recorded as not answering:

cargo test --manifest-path rust/Cargo.toml -p tama-cli --test inline_execution_judged
cargo test --manifest-path rust/Cargo.toml -p tama-cli --test pre_bash_throwaway
cargo test --manifest-path rust/Cargo.toml -p tama-cli --test pre_bash_trajectory_runs

With TAMA_TEST_INLINE_JUDGE=1 or TAMA_TEST_THROWAWAY_JUDGE=1 and -- --ignored the first two ask the real judge: python3 -c and a shell loop refused and a commit message passed; a keyword search, gcloud compute instances list and an executed quarantined file refused, and listing the quarantine or reading one file passed.