Docs/CLI reference

CLI reference

tama violations find

Replays the shared content and file-size hooks, reporting the first violation per file and one finding per crowded folder without modifying the repositories. The CLI, repair command and MCP tool use the same scanner.

Replays the shared content and file-size hooks, reporting the first violation per file and one finding per crowded folder without modifying the repositories. The CLI, repair command and MCP tool use the same scanner.

#Invocation

tama violations find (--repo <path> | --tree <dir> | --owner <gh-owner> | --me) [...]
                     [--json | --jsonl] [--summary] [--size-only | --duplicates | --internal-names | --time-limits | --one-off-commands | --fallbacks] [--hook <id> | --program <path>] [--clone-dir <dir>]

Targets are repeatable and combinable. --repo names one directory; --tree discovers Git checkouts; --owner uses gh and shallow-clones missing repositories; --me selects the authenticated user and their organizations. --hook <id> replays one registered hook instead of the default pair, --program names a hook file to replay instead of this program, --clone-dir changes the clone cache, --json prints one structured report at the end and --jsonl one JSON line per step as it happens.

#Every step as it happens

A scan says what it is doing while it does it. On stderr it names the repositories, then before each hook run judging <path> with tama hook <id>, each finding as found <path> [<hook>]: <message>, each file a hook could not judge as not judged <path>: <cause>, and each repository's totals. A hook whose standard output holds a line that is not a decision document leaves its file not judged, with tama hook <id> printed a line that is not a decision document (<parser's cause>): <line>: that line is never skipped as if the hook had said nothing. Each hook run goes through Stado's one wait mechanism, so a hook that does not finish stands named on stderr — czekam: tama hook <id> judging <path> with where and since when — instead of the scan printing nothing until it ends. --jsonl prints the same steps on stdout, one object per line: {"event":"judging","repo","path","hook"}, {"event":"violation","repo","hook","rule","path","message"}, {"event":"unjudged","repo","path","message"}, {"event":"repository","repo","hook","mode","scannedFiles","violations","notJudged","via"} and {"event":"note","message"}, then one {"event":"totals","totals","problems"}. The in-process passes report their findings once each repository is read. --json with --jsonl, or either with --summary, is a usage error.

--hook <id> narrows the replay to one hook this program runs — check-numeric-literals or block-keyword-literals to count the numbers and word lists still in a repository, without the model-judged gates of the full write guard. An id the program does not run is refused before anything is scanned: --hook <id> is no hook this program runs; `tama hook` names the hooks it runs; a --hook beside an in-process pass is refused with --hook <id> chooses a hook of the replay, and this mode judges files in process without one; it cannot be combined with --program. A registered hook's refusal is read from the decision document it prints ({"decision":"block","reason":…}), the form every runner reads. Tama Desktop's Violations screen takes the same hook id in its field above the rules when the every-rule pass is selected, and its scan job (tama request violations/scan with "hook") refuses the same ids with HTTP 400. Real journey tests/violations/find/streaming.rs.

The file-size rule is read from block_oversized_files.sh beside the selected hook. A custom hook directory must contain that companion file; an unreadable or failing hook appears in errors and makes the scan fail. Each file is enumerated once, the 300-line boundary is checked, and .tama-violations-ignore applies to both content and size checks. The repair command uses this same result, so an oversized file cannot disappear from its work list.

The line limit does not apply to structured data, a drawing, a manuscript or a log, and the scan answers exactly as the hook does: .json, .jsonl, .ndjson, .toml, .yml, .yaml, .lock, .csv and .tsv are documents a loader reads whole — splitting a registry or a product catalogue breaks its reader — .svg is one picture a renderer wrote, .tex, .bib, .sty, .bst and .cls are one document with the venue's own length, and a .log is a recording of what a program did, one line per event, which nobody edits and nothing imports. A file under migrations/ is exempt too: it states what already ran, under the name the database recorded. The exemption is the format, not the name: toml_reader.rs is still source.

The folder limit leaves a database's own directories alone for the same reason — supabase/migrations and supabase/seeds hold numbered documents loaded in name order, which a sub-folder would rename — along with node_modules, .git, __pycache__, tests and any migrations directory.

The folder limit also counts only files somebody wrote and somebody else has to find again. A screenshot, a recording, an icon, a font, a drawing or an archive is not a module: a product that keeps one folder per journey holding a recording and one screenshot per state the journey passed through has a folder whose file count is the number of states, and "move files into sub-folders or consolidate" asks for something that cannot be done to two screenshots. The line pass skips those files as binary content, and the folder pass does not count them either. The modules beside a capture still count: four modules and twenty screenshots leave room for a fifth module, and the sixth is refused. The audit asks the guard itself, block-crowded-folders, so the two can never answer differently.

#Whose repositories are scanned

A --tree sweep scans the checkouts this workshop owns: the GitHub account signed in here and every organisation it belongs to, read the same way --me reads it. A checkout whose origin names anybody else is somebody else's product — a third-party tool checked out because we use it, every edit to which the guards refuse — so the sweep names the owner and scans nothing inside, and none of its files are reported as ours to move. An explicit --repo still scans whatever it is pointed at; a checkout with no origin, one on a host that is not GitHub, and every checkout when GitHub cannot be asked, all stay ours — a failed lookup must not turn our own code into somebody else's.

A skip is not a failure. It prints as SKIPPED <owner> <path> where a failure prints PROBLEM, the summary's last line says how many checkouts in the tree are owned elsewhere, and only real failures raise the exit status — a workshop with nothing left to fix has to be able to finish at zero while those checkouts sit beside it. A copy of somebody else's project that lives inside one of our repositories is a different case: it has no origin of its own, so the repository declares it in .tama/violations-ignore with the owner written beside it, the way wisent-ios declares the five upstream Swift packages vendored into the app.

#Which hook is replayed

Without --hook or --program, Tama replays the guards the registry runs on every write — its own tama hook pre-write-edit and tama hook block-oversized-files — so a scan reports what a write would really be refused for, read from the decision document each prints. The MCP server's find_violations tool and the desktop's scan and repair jobs use the same default. The retired shared-hooks/pre_write_edit.sh is never a default: the registry no longer runs it. A hook file named with --program is run as a file and refuses by exit status, as such scripts do. When a named hook file does not exist the refusal is exactly hook not found: <path>; when the default cannot be used (the running tama cannot name its own executable) the refusal keeps that first line and says so, with the ways forward: name a hook file with --program, or run --size-only. The journey cargo test --manifest-path rust/Cargo.toml -p tama-cli --test violations_mcp_default_guard requires the MCP and CLI defaults to replay the registered guard and a missing named hook file to be refused naming it.

--size-only answers the two size rules — one file over 300 lines, one folder over 5 files — in this process, over the same enumeration and with the same sentences, so it needs no hook at all and finishes a whole tree in seconds instead of hours. A --program named alongside it is reported as not replayed, and a --hook is refused. --summary prints one row per repository, worst first. Use the fast pass to choose what to fix, then re-scan that one repository without it for the content rules the hooks own.

#Capabilities written again

--duplicates skips the hook replay and reports code that writes again a capability another Wisent product owns. The capabilities, their owners and the code that re-implements each one are declared in one catalog, rust/crates/tama-violations/src/find_violations/duplicates/catalog.json, so a sweep reads every capability the catalog holds instead of one search somebody remembered to run. Today it holds:

  • private-database — a private SQLite store instead of a fleet database from stado database;
  • sql-client-outside-stado-database — a hand-written Postgres client, pool or connector (postgres::Client, tokio_postgres, an sqlx PgPool) instead of stado_database::connect and SeaORM entities; the write gate refuses the same calls in new code;
  • secret-outside-skarbiec — a credential kept in the Keychain or a dotenv file instead of in Skarbiec (a bearer a trusted launcher injects into the environment is the sanctioned path);
  • model-call-outside-brama — a model provider called directly instead of through Brama;
  • provider-egress-outside-integrations — a third-party provider called directly instead of through wisent-integrations;
  • browser-outside-weles — Playwright, Puppeteer or WebDriver driven directly instead of through Weles;
  • updater-outside-desktop-update — Sparkle driven directly instead of through wisent-desktop-update;
  • sign-in-outside-desktop-auth — a native sign-in flow instead of wisent-desktop-auth;
  • signing-outside-stado-product — codesign or notarytool by hand instead of stado product.

A repository is identified by its origin remote, not its directory name, and the owner's own repositories are exempt from their capability. Each hit names the capability, the line and the owner. A guard or test that quotes a signature as a string is reported too; read the line before recording it. A new capability is one catalog entry.

#Internal names and incident stories

--internal-names skips the hook replay and reports every line, not the first per file, that carries an internal detail. It is the judgement the block-internal-names write guard applies to what a write adds, run over whole files:

  • a fleet host name, host address or account name, anywhere in the file. The names are read from the Stado host registry and from this account, never from a list kept in Tama;
  • a vault item id in prose. The ids come from the current stado credentials ls --json answer; an older listing is never substituted when that read fails. An id shaped like an environment variable, a one-word id and an id equal to a repository's name are product names or settings and do not count;
  • a date, or a commit, build, job or defect id, in prose: the shape of a story about one incident. An id is seven or more hex digits holding a digit and a letter, its letters in one case; a run that mixes cases (a scheme or curve name) or is joined into a longer name (a code path segment such as crypto::…::verify, or the first word of a hyphenated crate name whose next word is not hex) is a name. A full UUID's groups are all hex, so it stays an id. The journey is tests/internal-names/compound.rs.

Prose is decided by the line's shape, not by the file's extension: a line made mostly of plain words, the text after a comment marker, and each quoted string. A date or a hash inside code is data and is not reported. Each finding prints as line N: <kind> `<value>`: <the line>. A source that cannot be read is reported under errors with the reason, because the names it would have supplied were not checked. A path whose content must carry such data is declared, with its reason, in the repository's .tama/violations-ignore.

A malformed vault listing is also an authority error: the document must be an array, and every item must have a string id. The write hook prints the failed authority and its cause with TAMA_HOOK_INFRASTRUCTURE, exits unsuccessfully and emits no partial policy decision. An unavailable authority is not permission to write. CLI and native scans retain the same errors, and the Desktop final rescan cannot report successful cleanup while any remain. Repair the named authority or its unreadable input before repeating the check; rewriting otherwise valid source does not repair the dependency.

Public product names also come from the local workspace and its release declarations. An absent workspace or absent release declaration contributes no declared name; an unreadable directory entry, unreadable declaration or malformed declaration is an authority error with its path and filesystem or JSON cause. Tama does not silently classify a product's name as internal after losing the declaration that establishes it as public. The real internal_names_writes journey covers a directory in place of a declaration and malformed persisted JSON through the built write hook; its recorded candidate and binary identity must accompany an authorized run.

The shell gate tama hook pre-bash uses the same host registry when deciding whether an SSH target belongs to the fleet. A registry read or JSON error refuses the operation with cannot determine the SSH target's fleet membership, its path and the underlying cause, rather than treating unavailable names as an empty fleet. The registered authority journey also exercises these failures through the real shell hook; it inspects the decision without opening an SSH connection.

The fix is a rewrite, not a deletion: describe the behaviour and the reason for it, read a host from the Stado registry at run time, and keep the incident itself in Oko.

#Waits and time limits

The workshop rule is that no repository waits a guessed interval, limits how long work may run, or kills work on a timer: when something breaks it fails with its own error, and when something has to happen first the code waits for that outcome, not for a number of seconds. --time-limits answers whether a repository keeps that rule. It skips the hook replay and reports every line the time-limit gate of pre-write-edit would refuse in a write — one finding per line, not the first per file, because a clean-up needs every site. The gate's own shapes are the definition, so the scan and the guard cannot disagree:

  • a sleep call in any language: the sleep of Python's time module, of Rust's std::thread and of Java's Thread, and C's usleep and sleep called with an interval;
  • the word for a time limit in any identifier, flag, field or sentence — a timer that fires to abort carries it in its own call — and the same limit spelled into any operation's name with separators inside the word (…_time_out), whatever the operation; no list of operations or timer actions decides;
  • the word for a due date on a line that also reads a clock — the current instant, compared or with a span added to it (Instant::now() + …, SystemTime::now().checked_add(…), … < Utc::now()), a monotonic clock, a span of time. The word alone names a stored date (a campaign's delivery date, a filing's due day) and passes;
  • the calls that end a task early: asyncio.wait_for, signal.alarm and the timer of AbortSignal;
  • the word time joined to limit, and max joined to time, as one word or across an underscore.

One spelling passes: switching a library's own limit off with (None), (0) or : 0. A library can carry a limit nobody wrote — an HTTP client that cuts every request at thirty seconds, a page wait that cuts at thirty — and deleting an explicit value hands the call back to that hidden one, so the repair for such a call is to turn the library's limit off, not to delete the line. Every line of a write is read, so a line that turns a limit off does not hide a later line of the same write that sets one (journey pre_write_edit_time_limits). Each finding reads line N: BLOCKED: … with the gate's own refusal. The pass needs no hook and judges a whole tree in process; --summary counts its findings in the first column. Tama Desktop runs it as the Waits and time limits pass of the Violations screen.

#Fallbacks

The workshop rule is that no code answers a missing or failed value with one nobody stated, or drops the error that says why: each such value is an error naming what is missing and carrying its cause. --fallbacks answers whether a repository keeps that rule. It skips the hook replay and reports every fallback the block-fallbacks gate would refuse in a write, found in each tracked file's syntax tree — one finding per construct and line, so the list is the whole debt and not the first site per file:

tama violations find --fallbacks --tree ~/code
tama violations find --fallbacks --repo ~/code/service --json

Each finding carries the hook block-fallbacks, the construct as its rule (Rust .ok() dropping the error, TypeScript ?? to a literal, Python except that only passes, Swift try? (error swallowed into nil), shell || true discarding the failure, …) and line N: <the line> as its message; reports are per repository, so a tree lists each product's debt apart. A construct named in a string or a comment is none. A file the pass cannot judge — its grammar does not load, or a declared construct is no query for it — is listed under errors with that cause, never as a file without findings. The pass needs no hook and judges a whole tree in process.

#Which checkouts a tree claims

--tree decides by provenance, not by a list of directory names. A checkout whose .git is a file is a linked worktree or a submodule, so its content belongs to the repository that owns it and it is not a target of its own. A checkout inside a path its owning repository ignores was written by a package manager, a cache or a scratch run — that ignore rule is the owner's own statement about provenance — so it is skipped too. Dependency and build directory names are still pruned, but only to keep the walk cheap.

Two checkouts that share an origin remote are one repository: the shallowest path is kept, with a lexicographic tie-break so the answer never depends on walk order, because fixing the source fixes every clone of it. A checkout with no origin remote is nobody's copy and is always kept, and a repository named with --repo is never deduplicated away.

#Output and state

Text mode groups findings by rule and ends with repository, violation, and discovery-problem totals. JSON returns repos, problems, and totals. Exit 0 means no violations or problems; any violation or discovery problem exits 1. Command-gating rules are out of scope because they inspect shell commands rather than file content.

#Refusals and failures

Missing targets, empty or missing option values, and unknown arguments print usage and exit 2. Missing paths produce exactly hook not found: <path>, repo not found: <path>, or tree not found: <path> and exit 2. When discovery yields no repositories, Tama writes no repositories found; it exits 1 if discovery also reported a problem, otherwise 0.

Return to the complete CLI command tree.