Repository navigation
feat(agents): read-only reviewed task catalog - #353
Merged
Merged
Conversation
…nspector Add a provisional target/result decision record and a read-only target inspector with CLI and MCP parity (HACK-1144, WU31). - `hack project target [--json]` resolves the exact project, checkout, runtime instance, optional service, env overlay selection and a Hack config generation from local files only. It performs no registry write, engine call or Git mutation, and reports runtime as not probed. - `hack.project.target` (read-only MCP tool) spawns the same command and passes selection through unresolved, so every resolution refusal is the CLI's. Its data is the CLI envelope re-validated against the contract. - Refusals are a closed, value-free catalogue (reason, stage, retry) that reuses existing error codes. `--expect-generation` refuses stale config; a config change during the read is refused instead of snapshotted. - Extract the Compose project name resolver from commands/project.ts so the inspector and commands share one implementation. - Paired CLI/MCP fixtures: two projects, a linked-worktree branch and an explicit instance, conflicting selectors, stale generation, ambiguous equal names, a moved checkout, detached worktree and no-write checks. Names, fields and reasons are provisional pending compatibility review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n gaps The read-only target inspector now refuses a Compose file that is not YAML, whose top level is not a mapping, or whose services key is not a mapping, as compose_parse_failed, instead of reporting a target with no services. Paired CLI/MCP fixtures now cover env selection (project default, explicit overlay, base), explicit instance slugging (Feature/X -> <base>--feature-x), two registrations owning one checkout, config_parse_failed, compose_parse_failed and generation_input_too_large. A stubbed CLI proves the MCP tool re-validates the envelope, and the validator's code/message consistency check has direct tests. Restores the buildEnvExplainArgs JSDoc. Refs HACK-1144 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add `hack project task list|inspect` and the MCP tools `hack.project.task.list|inspect` (HACK-1157, WU26-CATALOG). They read reviewed recipes for existing repository scripts from `.hack/tasks.json` and judge each one against the exact WU31 target: exact argv, Compose service, workdir, env scope, required capabilities, effects, prerequisites, checks, cleanup and pinned provenance (file and JSON pointer SHA-256 digests plus the canonical service definition digest). Nothing is executed. Results carry `authority: "none"` and `execution: "unavailable"`; there is no shell endpoint or execution path. Closed refusals cover ambiguous ids, untrusted (log/suggested) provenance, inline code or shell syntax in argv, host runners (no approved profile), missing capabilities, wrong env scope, unknown services, missing or escaping inputs and stale pins. Refused recipes are reported without their argv or text. The repository's own toolchain recipes (models, check, rust-check) are pinned and checked by the new test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new read-only target inspector accepts --env and --service, so the env matrix source check requires a row for it. It reports only the overlay selection and never reads env files or values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Inline-code detection no longer stops at the first operand: an inline or stdin flag anywhere after a shell or interpreter refuses the recipe, so option values before -c/-e (bash -o pipefail -c, python3 -W ignore -c, node -r x -e, the GitHub Actions bash form) are caught. - env, eval, xargs, su, ssh, awk and sed are refused at any argv position; watch with a command, Rscript/R -e, REPL/eval subcommands, '-' and a missing script operand (code from stdin) are refused too. - Pin paths exclude every .git segment, all .hack machine-local entries (.env, .env.state.json, local overlays, tickets) and secret/key files, compared case-insensitively for APFS. - readPinnedDigest re-checks the path with directory symlinks resolved, so a symlinked directory can neither leave the checkout nor alias excluded state; new fixtures cover both. Refs HACK-1157 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…aluators Task catalog pins must now be Git-tracked files, checked with one read-only `git ls-files` before any pinned byte is read, so a gitignored secrets file can never have its digest returned in a stale refusal. Machine-local entries are excluded under the legacy `.dev` project directory as well as `.hack`. The inline-command denylist now covers package runners (`npx -c`, `npm exec -c`, `pnpm -c`, `yarn exec`, `bun exec`), TypeScript runners, privilege wrappers, `git -c`/`--exec`/`submodule foreach`, `make --eval`, `deno run -`, Elixir and editor forms. Docs no longer describe the check as fail-closed beyond known programs. The registration test now proves the spawn recorder saw git calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The canary in tests/project-task-catalog.test.ts proves smuggled env values are never echoed. Gitleaks flagged its introducing commit, which the secret-scan job rescans on every push, so allowlist that exact line the same way as the MCP inspect-tool canary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both commands accept --env, so tests/env-command-matrix.test.ts requires a row for each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A filesystem fault while reading target inputs (a mode-000 or directory Compose file) escaped as E_UNEXPECTED with the raw OS message and absolute path. Map errno errors to the closed input_permission_denied and input_unreadable refusals, and let the adapter validator pass a detail-less error only for parser E_USAGE refusals. Strengthen the no-registry-write proof by comparing against the registry bytes written in beforeAll, and cover the validator's authority and runtime.observed checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up origin/next through the F17 branch so the task catalog is checked against the current base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ulia -e in task argv The task catalog's inline-command denylist missed three string-to-code forms: a `data:` URL module handed to Node, Bun, Deno or tsx (`node --import data:text/javascript,...`, `deno run data:...`), `git bisect run` and `git filter-branch`, which run their command strings through a shell, and Julia's `-e`/`--print`/stdin REPL. Recipes using them are now refused as `inline_command`; ordinary `node --import ./file.js`, `git log` and `julia script.jl` argv still pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the squash-merged AX target contract (#346) so the PR no longer conflicts with next. Conflicts were additive: the task catalog's command, MCP tools and docs rows are kept beside next's content. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d interpreters in task argv `git fetch`, `clone` and `ls-remote` run their `--upload-pack` value, and `git push` its `--receive-pack` value, as a shell command, so a recipe using them is now refused as `inline_command`. A dash version suffix (`node-22 -e`) now names the same interpreter as `node22`. Ordinary `git push -u origin main`, `git fetch origin` and `node-22 script.js` argv still pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
October 21, 2026 22:42
This was referenced Oct 21, 2026
roodboi
added a commit
that referenced
this pull request
Oct 22, 2026
…hecks locally (#381) ## Root cause - **HACK-1290:** `package.json` pins `packageManager: bun@1.4.2`, and `mise.toml`, `.tool- versions`, every workflow and the runtime images use 1.4.2. Nothing checked the Bun that actually ran local gates, though. A Bun ahead of mise's shims on `PATH` (here mise's `latest` 1.4.3) skips the checkout's `mise.toml`. A Bun 1.4.2 macOS SIGSTOP difference made a test pass locally on 1.4.3 and then fail in CI. - **HACK-1294:** the compiled-bundle CLI checks (`scripts/check-config-compiler-cli.ts`, `scripts/check-native-{env,host,routing,endpoint,process,acquisition}-plan-cli.ts`, `scripts/check-native-config-import-cli.ts`) ran only inside CI's macOS "Config compiler" job. No local command ran them, so a behavior change passed every local gate and failed CI. ## Fix - New `scripts/check-bun-version.ts` (`bun run check:bun-version`). It compares `Bun.version` with the exact `packageManager` pin. On a mismatch it exits 1 with the fix: `mise x bun@<pin> -- bun run <script>`. Setting `HACK_ALLOW_BUN_VERSION_MISMATCH=1` (exactly `1`) continues with a warning for a deliberate experiment. - The guard runs first in root `test`, `check` and `typecheck` and in `packages/cli` `test`, `check` and `typecheck`. `turbo.json` passes the override through via `globalPassThroughEnv`, so package tasks honor it. CI already installs 1.4.2, and the `.hack` toolchain image installs from `mise.toml`, so both pass unchanged. - New `bun run check:native-plan-cli` (`scripts/check-native-plan-clis.ts`). It runs the eight compiled-bundle checks one at a time, in CI order, and stops at the first failure. It refuses to start without `dist/hack` and `dist/hack-config-compiler`, and tells you how to build them. - `AGENTS.md` and `.ai/skills/hack-repo-verify/SKILL.md` document the guard, the override and the new command. These are contributor-only files. Consumer guidance (`src/agents/instruction- source.ts`) did not change, so no plugin regeneration or render fingerprint is needed. ## Tests - `tests/bun-version-guard.test.ts` covers four things. First, the match, mismatch and override verdicts (only `"1"` overrides) and the rejection of inexact pins. Second, the real script exits 1 on a mismatch and 0 with the override, run in a temp copy with an isolated `HOME`. Third, `mise.toml`, `.tool-versions` and every literal workflow `bun-version` agree with `packageManager`. Fourth, every entry point starts with the guard, and turbo passes the override through. - `tests/native-plan-cli-wiring.test.ts` fails when any `scripts/check-native-*-plan-cli.ts`, the config-import check or the config-compiler check is missing from the local command or from a CI step. It also checks that the runner refuses to start without the built bundle. - Mutation-proven: each of these 8 mutations made at least one test fail, and the restored code passes 8/8. - Always match. - Treat any truthy override as `1`. - Drop the exit code on a mismatch. - Remove one check from the list. - Remove the guard from root `test`. - Remove the guard from CLI `typecheck`. - Remove the turbo pass-through. - Ignore missing prerequisites. ## Evidence (macOS arm64, local) - Plain `bun` is 1.4.3: - `bun run check:bun-version` exits 1 with the message. - `bun run test` stops before turbo. - `HACK_ALLOW_BUN_VERSION_MISMATCH=1 bun run typecheck --force` warns at the root and inside `@hack/cli:typecheck`, which proves the pass-through, and both tasks succeed. - `mise x bun@1.4.2 -- bun run check:bun-version` exits 0. - `bun run build`, `bun run build:config-compiler` (rustc 1.97.1), then `mise x bun@1.4.2 -- bun run check:native-plan-cli` all pass. All eight checks pass, including acquisition (198 checks) and config import (49 checks). - Bun 1.4.2 gates: - `bun run --cwd packages/cli typecheck`: 0 errors. - `bun run --cwd packages/cli check`: 0 errors (existing warnings only). - `bun run check`: passes. - `bunx ultracite check` on the changed files: clean. - `bun run test` (full suite, Bun 1.4.2, host load average 50 to 130 from parallel agents, 2h07m): the guard passed and turbo ran. 6785 tests passed and 1197 failed across 120 files. All 5 new tests passed in the suite. - I reran each of the 120 failing files serially with `--timeout 60000`. 99 files passed. 21 files still fail, all `native-compose-*` / `native-authored-*` source-CLI fixtures. Most fail with the fixture's inner 15 s `SIGKILL` (exit 137) or `private-uncertain` refusals. - Baseline A/B with `origin/next`'s `package.json`, `packages/cli/package.json` and `turbo.json` restored on the same host gave identical results. `native-compose-warm-up- command`: same exit 137. `native-compose-down-command` plus `native-compose-storage-fence- cost`: 1 pass / 17 fail in both runs. - Direct `bun test` does not run any code this PR changes, so I count these failures as host and environment failures, not as caused by this change. I did not get a fully green local suite on this host. - gitleaks 8.30.1 over `origin/next..HEAD`: no leaks. ## Remaining gaps - Raw `bun test <paths>` and `bun index.ts` do not run the guard. Only the `bun run` entry points do; this is documented. - `check:native-plan-cli` covers the CLI checks only. CI's "Config compiler" job also lints those scripts, runs their transport tests, runs `build:release` and validates the schema corpus in Python. The docs say to lint changed scripts explicitly. - This head (`d9d6376d`) is based on `ec223cdb`, the commit I ran the full suite against. `next` has since gained four unrelated commits (#353, #340, #344, #356). None of them touches these files, and the head merges cleanly (`git merge-tree`). The focused tests also pass after rebasing onto `2f434217`. - Hosted CI on this head has not been observed yet. - Found while testing, not fixed here: `tests/setup-migrate-command.test.ts` resets `root = ""` in `afterEach`. A CLI call that outlives its timed-out test then passes `root: ""` to `privateCliEnv`. That resolves to the checkout, so `isolatedDockerClientEnv` writes `docker- client/config.json` into the repository root. I saw this during the loaded full run. `privateCliEnv` should refuse an empty or relative `root`. Refs HACK-1290 Refs HACK-1294 🤖 Generated with [Claude Code](https://claude.com/claude-code)
roodboi
added a commit
that referenced
this pull request
Oct 22, 2026
## Round 3 (`d2bc9446`, `c31f4dbd`) The branch was started from the remote head `97b1e678` and merged with current `next` (`5c9cc82e`, merge `25b949cf`). It was then fixed in `d2bc9446`, and `c31f4dbd` fixed a security-review finding on that commit. Both pushes were fast-forwards. ### Bypasses fixed An independent verifier ran code through three bypasses at `97b1e678`. 1. **Python trampoline modules.** These modules run a module, script or callable that is named in their own arguments: - `python3 -m runpy timeit -n1 '<code>'` - `python3 -m trace --listfuncs --module timeit '<code>'` (also `--module code`) - `cProfile`, `profile` and the Python 3.15 `profiling.tracing -m -s time <module>`. Here `-m` is a flag and the module is a later operand, so the existing `-m` scan saw `-s`. - `unittest`, which calls the dotted name it is given when that name is callable. `unittest code.interact` starts a REPL, and `unittest builtins.breakpoint` and `sys.breakpointhook` start `pdb`. I confirmed each of these locally on Python 3.14.9 (and `profiling.tracing` on 3.15), using code that only prints a marker. `runpy`, `trace`, `cProfile`, `profile`, `profiling` and `unittest` are now trampolines. Every argument after one is checked against `PYTHON_REPL_MODULES`. This is fail-closed, and it also covers a trampoline that names another trampoline (`runpy runpy timeit …`). `builtins` and `sys` were added to the REPL modules for the `unittest` callable form. `pdb` was already refused. The same check runs through `uv run -m`. 2. **`psql -o '|cmd'`.** psql 17's `parse_psql_options` calls `setQFout(optarg)`, and `openQueryOutputFile` then `popen`s a value that starts with `|`. This happens before psql connects. I read this in the `REL_17_STABLE` source. A new evaluator field, `pipeOptions`, makes `-o` and `--output` refuse a `|` value in every spelling: - `-o '|cmd'` and `-o|cmd` - `-qo|cmd` and `-qo '|cmd'` - `--output=|cmd` and `--output '|cmd'` - `--out=|cmd` (an abbreviation) psql's other file options are `-f` (`process_file`) and `-L`/`--log-file`. Both use plain `fopen`, so a `|` there is a file name and stays accepted. This is documented. 3. **Dashed Git builtins.** A program whose basename is `git-<sub>` (the basename after `.exe` is removed, including from an absolute git-core path) now maps to the Git evaluator with `<sub>` as its first argument. The existing `clone -u`, `--upload-pack`/`--exec`, `!` alias and `submodule foreach` rules then apply to it. ### Non-blocking fixes - `python3.12-dbg`, `python3-dbg`, `python3-intel64` and `python3.13-intel64` are now normalized to `python`. A new `VARIANT_SUFFIX` is stripped before the version suffix. - `duckdb -storage-version VER db` is accepted, because `-storage-version` is now one of DuckDB's value-taking options. A SQL operand after the database is still refused. - `python3 -mpytest` is accepted. An attached `-m<module>` counts as the program's operand. The cluster is read only up to a value-taking option, so `python3 -Ximporttime` is still refused as stdin. `-mcode` and a bare `-m` are still refused. - The docs and the code comments now say that MariaDB's `my_getopt` still accepts unique prefixes. Refusing `--exec` is therefore required for `mariadb` and only fail-closed for `mysql`. `mariadb --exec` and `--exec=` are now tested. ### Security-review finding on `d2bc9446`, fixed in `c31f4dbd` The finding was MEDIUM: a parser differential. The first version of `selectsModule` treated any `-m` as selecting a module. A `-m` that was the value of a preceding value option therefore skipped the no-operand refusal, and Python read code from stdin. On Python 3.14.9, piping `print(...)` into each of these ran it: - `python3 -W -m --` - `python3 -W -m -X dev` - `python3 -X -m -X dev` - `python3 -X -mpytest` - `python3 -bW -mpytest` `--check-hash-based-pycs -m` exits on the invalid value, so refusing it is fail-closed. `selectsModule` now walks the options the way Python and `operands()` read them: - A separate value option (`-W`, `-X`, `--check-hash-based-pycs`) consumes the next argument. - A cluster that ends in a value option (`-bW`) consumes the next argument too. - An attached value (`-Wignore`, `-W-m`) ends the cluster. - A separate module name must not start with `-`. - An operand or `--` ends the walk. The cluster reading is split into `pythonClusterEnd`, which keeps biome's complexity limit. Tests: - Refused: `-W -m --`, `-W -m -X dev`, `-X -m -X dev`, `-X -mpytest`, `-bW -mpytest`, `--check- hash-based-pycs -m -X dev`, `--check-hash-based-pycs -mpytest`, `-W-m --`, `-Xdev -W-mpytest`, `-Wdefault -m -X dev` and `-m --`. - Accepted: `-W ignore -mpytest`, `-Wignore -mpytest`, `-X dev -mpytest`, `-bW ignore -mpytest`, `--check-hash-based-pycs always -mpytest` and `-X dev -m pytest`. Mutation proof: 6 mutations. These 5 were killed by `an attached -m module is the program's operand`: - `selectsModule` reverted to the `d2bc9446` version - the separate value option not skipped - a cluster ending in a value option not taking the next argument - an attached value treated as taking the next argument - a module name allowed to start with `-` The 6th removes `--` as a stop, and it survives. It is equivalent: `operands()` already turns everything after `--` into an operand, so `selectsModule` only runs when nothing follows `--`. The check is kept as a guard for reuse. ### Tests and mutation proof `tests/project-task-catalog.test.ts` gains four rule tests: - `psql -o/--output piped to a command` - `duckdb -storage-version takes a value` - `an attached -m module is the program's operand` - `Python trampoline modules and their targets` It also gains a `dashed Git builtins` test, and new cases in the launcher-name and SQL rules. A collateral case pins the fail-closed side effect: `python -m cProfile scripts/x.py code` is refused. Each new rule pairs refused and accepted argv. I ran 20 mutations. Each one reverted a single fix on a copy of the source, and the source was then restored. All 20 were killed by the expected HACK-1331 test, and the baseline was 15/15 pass: - every trampoline off, and then each of `runpy`, `trace`, `cprofile`, `profile`, `profiling` and `unittest` removed in turn - `builtins` removed, and `sys` removed - psql `pipeOptions` off - the long-name prefix ignored, the separate value ignored, and the short or cluster form ignored - dashed Git off - `VARIANT_SUFFIX` off - duckdb `-storage-version` off - the attached `-m` operand off, and the attached `-m` scan ignoring value options - MariaDB prefixes off No existing refusal was removed or loosened. ### Evidence (Bun 1.4.2 via `mise x bun@1.4.2`, on this Mac) The same gates passed at `d2bc9446` and again at `c31f4dbd`: - `bun run --cwd packages/cli typecheck`: exit 0. - Root `bun run check`: exit 0, with `privacy-check: ok`. Its warnings are existing ones; none are in the changed files. - `biome check` on `src/lib/task-catalog-recipe.ts` and `tests/project-task-catalog.test.ts`: clean, with no warnings. - `tests/project-task-catalog.test.ts`: 33 pass, 0 fail. - gitleaks 8.30.1 `dir` over a `git log -p origin/next..HEAD` export (`origin/next` = `5c9cc82e`): no leaks. No pinned file changed, so `.hack/tasks.json` was not re-pinned. Hosted CI: run 39839933536 at `c31f4dbd` succeeded. All 17 jobs passed, including `test` (the full suite), all four `docker-e2e` shards, `linux-process-lifetime` and `secret-scan`. The `d2bc9446` run was superseded by the follow-up push. ### Still open after round 3 - The trampoline check is fail-closed. A script argument named like a REPL module after a trampoline is refused (`python -m cProfile script.py code`), and so is a script file named `timeit.py`. This is documented. - Other stdlib modules that load code from a file, such as `python -m pickle file.pkl`, are not refused. That code comes from a file, not an argv string. - psql `-f -` (SQL from stdin) is not refused, which is consistent with the existing stdin gap for SQL clients. --- ## Root cause The read-only task catalog (#353) refuses recipes whose argv evaluates inline code, using a denylist table in `src/lib/task-catalog-recipe.ts`. Four gaps reported in HACK-1331: 1. SQL clients were not in the table, so `psql -c`, `mysql -e`, `mariadb -e`, `duckdb -c` and `sqlite3 app.db 'SQL'` passed. Statements can also reach the host shell (`psql` `\!`, `mysql` `system`, `sqlite3` `.shell`). 2. `python -m code` (and other REPL or eval modules) passed. The `-m` value was treated as an ordinary option. 3. Git spellings slipped through: `git clone -u <cmd>`, abbreviated long options (`--upl=cmd`, which Git's parse-options accepts), a value attached to a short option (`-u/path/cmd`), and `!` alias values written with `git config alias.x '!cmd'`. I confirmed in a scratch repository that `--upl=` and `-u<cmd>` both run the command on Git 2.56. 4. The tracked-pin `git ls-files` query inherited the caller's environment. A caller's `GIT_INDEX_FILE` or `GIT_DIR` could point it at an index that tracks a gitignored secret, which would make that file pinnable and its digest visible in a stale refusal. Verification of `ef52aa53` found two more bypasses, fixed in `bef66ecf`: 5. `duckdb --c`, `--s` and `--cmd` were accepted. The sqlite3 shell, whose argument parser DuckDB shares, strips one leading dash, so these are `-c`, `-s` and `-cmd`. The verifier ran `duckdb --c ".shell echo …"` on DuckDB v1.5.6 and it reached the host shell. `--cmd` was also in DuckDB's value options, so the flag and its SQL were both skipped. 6. `git --attr-source <tree> clone -u <cmd>` was accepted. `--attr-source` was not a known Git global value option, so its value (`HEAD`) was taken as the subcommand and the `clone`-scoped `-u` never applied. On Git 2.56 this form runs the `-u` command. ## Fix - New data-driven evaluator fields, all in the same `EVALUATORS` table: - `codeOperandsAfter`: `sqlite3` and `duckdb` treat any operand after the database as SQL. - `clusters`: off for programs whose one-dash options are words (`sqlite3 -csv`, `duckdb -json`). - `abbreviates`: for Git, `psql`, `mysql` and `mariadb`, a prefix of a refused long option matches. So does a value attached to a refused one-letter option, case-sensitively. - `replModules`: for `python`/`pypy`, the `-m` value matches on its first dotted segment. Covered spellings: `-m code`, `-mcode`, `-um code`. The modules are `code`, `idlelib`, `asyncio`, `_pyrepl`, `pdb`, `timeit`, `sqlite3` and the IPython family. - `subcommandInline`: `-u` counts only under `git clone`, so `git push -u` and `git add -u` still pass. - `bangCommands`: for Git, a value starting with `!` (a whole argument or after `=`) counts. - Git also gains `--config` (clone's long `-c`) and global value options, so the subcommand is found after `--git-dir x`. - `foldsDoubleDash` (new in `bef66ecf`): for `sqlite3` and `duckdb`, `--x` is read as `-x` when matching inline flags and value options. The two-dash entries in `SQLITE_VALUE_OPTIONS` and sqlite's `--cmd` inline entry are now redundant and removed. - `subcommandInline` (changed in `bef66ecf`): a subcommand's scoped flags apply when any operand names it, not only the first. A Git global option missing from `valueOptions` can no longer hide `clone`. `--attr-source` is also added to Git's global value options. This is fail- closed: `git push -u origin clone` (a branch or remote named `clone`) is now refused, and that is documented and tested. - `exec()` now accepts `unsetEnvKeys`. `allTrackedByGit` drops every inherited `GIT_*` variable. - The documented fail-closed collateral is kept: `git -C dir status` is still refused. - `docs/reference/ax-task-catalog.md` is updated with the refused and accepted forms, the evidence rows and the known gaps. ## Tests `tests/project-task-catalog.test.ts`: - Five rule tests (SQL flags, SQL operands, Python modules, `git clone -u` and upload-pack spellings, Git `!` aliases and `--config`). Each pairs refused argv with accepted argv. - A collateral test for `git -C dir status`. - A domain test that sets `GIT_INDEX_FILE`, `GIT_DIR` and `GIT_WORK_TREE` in the caller's environment, pointing at an alternate index that tracks the secret. The secret stays `input_path_invalid` and its digest does not appear. A positive control shows the alternate index really does track the file. A foreign `GIT_DIR` does not hide the checkout's own index. Mutation proof: 19 mutations were run, each disabling one rule. All 19 were killed by the expected test. Examples: - removing the `psql`/`mysql`/`duckdb` flags - turning abbreviations off - turning clusters back on for sqlite/duckdb - `codeOperandsAfter: null` - emptying `replModules` - dropping attached `-mpdb` parsing - removing the `clone -u` scope - turning off attached short values - removing the Git global value options - turning off `bangCommands` - removing `--config`/`--config-env` - removing the `GIT_*` scrub - removing the case folding of `-C` `git -c alias.x=!cmd` is caught by both the `-c` rule and the `!` rule, so it is proven only by the mutation that turns off both. `bef66ecf` adds cases to the existing rule tests: - Refused: `duckdb --c`, `duckdb --s`, `duckdb --cmd … data.duckdb`, `sqlite3 --cmd …`, `git --attr-source HEAD clone -u id url`, and `git --future-global value clone -u id url` (a global option the table does not know). - Accepted: `sqlite3 --separator , app.db`, `duckdb --f scripts/x.sql data.duckdb`, and `git --attr-source clone add -u` (here `clone` is the tree-ish). - Collateral: `git push -u origin clone` is refused. Mutation proof for `bef66ecf`: 7 mutations, all killed by the expected HACK-1331 test. They were: folding turned off everywhere, folding removed from value-option parsing, folding removed from flag matching, `foldsDoubleDash: false` for duckdb, `foldsDoubleDash: false` for sqlite, `--attr-source` removed from Git's value options, and scoped flags limited to the first operand. The file was restored byte for byte afterwards. I also checked the two-dash spellings against the real tools: `sqlite3 --separator ,`, `--cmd`, `duckdb --f`, `--s` and `--separator` all behave like their one-dash forms (sqlite3 on macOS, DuckDB v1.5.6). ## Evidence - Bun 1.4.2 (`mise x bun@1.4.2`). - At `bef66ecf`, on this Mac: `bun run check` (includes `privacy:check`) exit 0. `bun run --cwd packages/cli typecheck` exit 0. `bunx ultracite check` on the changed files is clean. `tests/project-task-catalog.test.ts`: 25 pass, 0 fail. gitleaks 8.30.1 `dir` over an `origin/next..HEAD` `git log -p` export (`origin/next` = `616407a5`): no leaks. A `git merge- tree` against `origin/next` is clean, and next has not changed the three touched files. - **Full suite at `bef66ecf`: not run.** The shared M3 lock (`~/.hack-v5-private/m3-ci/.lock`) was held from 09:19 with no test process running. Another unit took it at 10:09, and at least seven more units are queued. The disk had 59 GiB free against a 58 GiB floor. I did not remove a lock I don't own. The change since `ef52aa53` only touches the argv denylist table, its tests and its doc. - Hosted CI at `bef66ecf`, attempt 1: every job passed except `docker-e2e shard 2/4`, and `test` did not finish. The shard 2 failure is `native-config-compose`: `hack exec db -- psql …` failed with `E_COMPOSE_FAILED` at the `storage-helper-start (probe-timeout)` boundary. That is the native Compose storage helper, not the task catalog: `isInlineCommandArgv` is used only by the task catalog, `project task` and MCP. The same shard passed at `ef52aa53`. A second attempt was started by someone else while `test` was still running, and it ended in `startup_failure` with no jobs. So hosted `test` has no result at this head. - At `ef52aa53`, on this Mac: `bun run check` (includes `privacy:check`) exit 0. `bun run --cwd packages/cli typecheck` exit 0. Focused `tests/project-task-catalog.test.ts`: 25 pass, 0 fail. `tests/cli-spawn-env-isolation.test.ts` and the `shell-*` tests: 27 pass, 0 fail. - gitleaks 8.30.1 `dir` over an `origin/next..HEAD` `git log -p` export: no leaks. - Full suite on the M3 (`bun run test`, Bun 1.4.2) at the exact pushed commit `ef52aa53`, run in a detached linked worktree: 7654 pass, 33 fail. The 33 failures are in 8 files. I reran each of those files serially with `--timeout 60000`: - `native-authored-project-oneoff`, `native-authored-process-incarnation`, `native-authored- project-recovery`, `native-compose-adoption-env-inputs`, `native-compose-adoption-generation`, `native-compose-down-command`, `native-compose-saved-command`: all pass (`e2e/run-dependency- cache` was also rerun and passes). They pass when rerun serially, and none of them exercise the task catalog. Most were 5 to 30 second timeouts under full-suite load. - `project-task-catalog`: 24 pass, 1 fail. The failure is `repository catalog > the checked-in .hack/tasks.json recipes are all reviewed`, explained below. - An earlier M3 run at `9b87447a` found one failure caused by this branch: the env-isolation scanner (`tests/cli-spawn-env-isolation.test.ts`) flagged a whole-environment `Object.assign` on `process.env` in the new tracked-pin test. `ef52aa53` sets and restores each `GIT_*` key with `restoreEnv` instead; the scanner test now passes locally. - That run also failed `repository catalog > the checked-in .hack/tasks.json recipes are all reviewed` with `E_USAGE` `detached_checkout`. This is environmental: the M3 runs from a detached linked worktree, and the same test fails the same way in the M3 runs for other units (G35, G37, F20). It passes locally on an attached branch, and it fails again in the serial M3 rerun at `ef52aa53`. Hack refuses to pick an instance implicitly in a detached linked worktree, so this test depends on how the checkout was made, not on this change. - No CLI surface, agent guidance or `.hack/tasks.json` pinned-file changes, so there was no `docs:cli-reference`, `generate:agent-plugins` or re-pin. ## Gaps - The check is still a denylist. SQL clients that read statements from stdin (`psql db`, `mysql db`, `sqlite3 app.db` with no SQL operand) are not refused; only a statement string in argv is. - Git config writes other than `!` aliases are not detected. Examples: `git config core.pager cmd`, `core.sshCommand`, `include.path`. They persist into `.git/config` and can run commands later. - New fail-closed collateral, now documented: - `mysql -E` and an attached `mysql -Dname` whose name contains an `e` - Git arguments with a `!` value such as `--grep=!x` - `python -I` was already refused by the `-i` rule - `git push -u origin clone`, or any Git argv with an operand `clone` and a `-u` flag - `mysql`/`mariadb --pager` (string to shell) is not covered. - The M3 full suite has not run at `bef66ecf` (see Evidence). Its last run was at `ef52aa53`. - On `ef52aa53`, hosted `linux-process-lifetime` failed once in `tests/shared-host- lifecycle.test.ts` ("clients in different time zones agree on an owned helper's birth"). That test checks `isAlive(owned)` right after release with no wait. This PR does not touch that path; `unsetEnvKeys` in `shell.ts` defaults to `[]`. Refs HACK-1331 🤖 Generated with [Claude Code](https://claude.com/claude-code)
roodboi
added a commit
that referenced
this pull request
Oct 23, 2026
…g check runs in detached worktrees (#409) ## Root cause `tests/project-task-catalog.test.ts` › "repository catalog › the checked-in .hack/tasks.json recipes are all reviewed" called `listTasks` on the repository root with no runtime instance. Every task-catalog result embeds one exact `hack.target.v0` target (catalog contract decision 7). The target inspector refuses a detached linked worktree with no named instance as `E_USAGE detached_checkout` (`src/lib/target-inspect.ts`, `resolveTarget` → `resolveEffectiveBranch` returns `detached-worktree`). The M3 CI worktrees are made with `git worktree add --detach`, so the check failed on every M3 full-suite run. It passed on branch checkouts. The test came in with #353. ## Fix I chose the documented-test route rather than making catalog reads instance-free: - Recipe judgement does not use the instance. It uses the checkout files, the Compose services, the env selection and the generation. But the catalog contract says every result embeds the **full, exact** target, and target refusals pass through unchanged. The target contract says a target always includes the runtime instance, and it lists the detached-worktree refusal as a paired-fixture case. - To make the catalog instance-free, a result would need a partly resolved target (`instance.source: "unresolved"`, `compose_project: null`), or a target shape just for the catalog. Either one changes the provisional `hack.target.v0` contract, its parsers and MCP re- validation. That decision belongs to the AX compatibility review, not to a test fix. Falling back to the base instance would break the catalog's "no fallback" rule. Changes: - The repository check now goes through `reviewCheckoutCatalog`. It names a fixed review instance (`branch: "task-catalog-review"`), so the result no longer depends on how the checkout was made. A comment at that helper says why. - `docs/reference/ax-task-catalog.md` (decision 7) now says that a detached linked worktree is refused as `detached_checkout` unless the caller passes `--branch` (MCP `branch`). No product code changed. The CLI surface, agent guidance and pinned files are unchanged, so no regeneration or re-pinning was needed. ## Tests New case: "repository catalog › the reviewed-recipe check runs in a detached linked worktree, where implicit instance selection stays refused". - It builds a committed one-recipe project in the fixture temp root and runs `git worktree add --detach` from it, using the existing private-env `git` helper with no global or system config. - It asserts that the fixture is detached (`rev-parse --abbrev-ref HEAD` returns `HEAD`). Then `reviewCheckoutCatalog` lists the recipe as `reviewed`, with `checkout.kind: "linked"`, `git_branch: null` and an explicit instance. - It asserts that a command that has to select an instance still refuses closed with exactly `targetRefusalEnvelope({ reason: "detached_checkout" })`: in-process `listTasks`, `inspectTask` and `inspectTarget`, plus the source CLI `hack project task list --path=<linked>` (exit 1). The CLI with `--branch` exits 0 and reports the recipe as reviewed. All new files are under the test's `mkdtemp` root. The test does not touch HOME, `~/.hack`, Docker, the keychain or the repository's own Git metadata. ## Evidence - Bun 1.4.2 (`mise x bun@1.4.2`), on a local Mac. - Reproduction: I detached HEAD in my own worktree (`git checkout --detach`). The original test failed with `{"code":"E_USAGE",…"reason":"detached_checkout","stage":"instance"…}`. With the fix, both repository-catalog cases pass on the detached worktree, and again after re-attaching the branch. - Mutation 1: I removed `branch: REVIEW_INSTANCE` from the helper. On a detached checkout both cases fail with `detached_checkout`. On a branch checkout the old test still passes, but the new regression case fails. That is the gap the old test had. - Mutation 2: I removed `throw new TargetRefusal("detached_checkout")` in `target-inspect.ts`, which turns it into a silent fallback. The regression case fails, so it pins the closed refusal. - Local gates: `tests/project-task-catalog.test.ts` passed 19/19. The catalog and target test files together (`project-task-catalog`, `project-target-*`) passed 47/47. Root `bun run check` (includes `privacy:check`) exited 0. `bun run --cwd packages/cli typecheck` exited 0. `biome check` on the changed test reported 0 errors. gitleaks 8.30.1 (`gitleaks dir` on the `git log -p origin/next..HEAD` export) found no leaks. - M3 full suite at the exact pushed commit, in a detached worktree: pending (queued behind the shared M3 CI lock; this description will be updated). ## Gaps - M3 full suite pending (see Evidence). Refs HACK-1358 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: HACK-1157 (WU26-CATALOG). Stacked on #346 (
wf/f17-ax-contracts-b16, WU31 target contract); review that first.Final behavior
hack project task listandhack project task inspect <task>, plus MCP toolshack.project.task.listandhack.project.task.inspect. Names and schemas are provisional; seedocs/reference/ax-task-catalog.md..hack/tasks.json(hack.tasks.v0), kept separate fromhack.config.jsonso the published config schema and the WU31 generation do not change. Each recipe names a Compose service and exact argv, workdir, env scope (overlay plus key names only), required capabilities, effects, prerequisites, checks, cleanup, and provenance pins: SHA-256 of whole files, of one JSON value (RFC 6901 pointer, for example onepackage.jsonscript), and of the canonical Compose service definition.hack.target.v0target. Target refusals pass through unchanged, and a config generation change during the read is refused.authority: "none"andexecution: "unavailable". There is no shell endpoint and no execution path. Refused recipes are reported by index, id and reason only; their argv, text and any smuggled env values are never echoed.recipe_invalid,ambiguous_task,untrusted_provenance,inline_command,unapproved_host_profile,missing_capability,env_scope_mismatch,service_not_found,input_path_invalid,input_missing,stale_recipe(with observed digests). Whole-catalog refusals:invalid_task_id,task_not_found,catalog_parse_failed,catalog_invalid,catalog_too_large,catalog_changed_during_read.execany word (this repo's toolchainrun.shdoes). An inline-code or stdin flag anywhere after a shell or interpreter refuses the recipe, even after an option value or a script path. Sobash -o pipefail -c,sh -O extglob -c,bash --rcfile x -c,python3 -W ignore -c,node -r ./x.js -e,perl -I lib -eand the GitHub Actions formexec bash --noprofile --norc -e -o pipefail -c …are allinline_command.env(includingenv -S),eval,xargs,su,ssh, awk and sed are refused at any position;watch <cmd>,Rscript -e,R -e,flock -c,bun repl,deno eval|repl,sh -s,-i,-and a shell or interpreter with no script operand (code from stdin) are refused too. Reviewed script argv still passes, for examplebash -o pipefail scripts/check.sh,python3 -m pytestandbun run check..gitsegment, any.hackdirectory's machine-local entries (.internal,.branch,.env,.env.state.json,hack.env.local.yaml,hack.env.*.local.yaml,tickets), or name a secret or key file (.hack.secret.key, legacy Hack secrets files,.env/.env.*,*.key,*.pem,*.p8,*.p12,*.pfx). A stale refusal reports the observed digest, so these exclusions keep value-derived digests out of output. Names are compared NFC-normalized and lowercased for case-insensitive APFS (.GIT/config,.hack/.Internal/x).readPinnedDigestre-runs the same path rules on the checkout-relative path with directory symlinks resolved. This replaces the old prefix containment check: a symlinked directory can neither leave the checkout nor alias excluded state such as.hack/tickets.checkout_read,checkout_write,named_volumes,network_egress,managed_env. Never granted: host ports/network, Docker socket, home mount, privileged, SSH agent, credential store. No host profile is approved, so host recipes are refused rather than run unconfined.models,checkandrust-check. They are unaffected by the stricter rules and are stillreviewed.Validation (local, macOS, Bun 1.4.3)
Rework head
267f62be:bun test tests/project-task-catalog.test.ts tests/integration-ownership.test.ts tests/cli-spawn-env-isolation.test.ts tests/mcp.test.ts: 44 pass, 0 fail. The catalog file alone: 17 pass, including the repo-catalog check.bun run --cwd packages/cli typecheck: 0 errors.bun run --cwd packages/cli check: 0 errors (34 existing warnings). Biome on the changed files: clean.bun run docs:cli-reference: no diff (the CLI surface did not change). No agent guidance or plugin render changed.From the original head
ee3704fd(not rerun on the rework head):tests/mcp-inspect-tools.test.ts,tests/project-target-inspect.test.ts,tests/cli-reference-docs.test.ts,tests/agent-instruction-source.test.ts,cli-help,doctor-generated-files,project-target-argument-redaction-cliandpattern-examplespassed;bun run typecheckandbun run checkhad 0 errors.Negative controls (each proved by a mutation that makes its test fail)
Rework mutations (all killed):
log-actionsfixture).env/eval/xargs/awk/sed refused only at argv[0]; stdin flags ignored; missing script operand ignored;-operand ignored; REPL/eval subcommands ignored;watch <cmd>ignored: each fails the grammar test..gitrefused only as the first segment; secret basenames accepted;.hacklocal entries cut back to.internal/.branch: each fails the path test.readPinnedDigestdisabled: the discovery fixture fails (escape-dirthroughlinked -> ../outside, andalias-localthroughhack-link -> .hackreaching.hack/tickets/notes.md, becomestale_recipeinstead ofinput_path_invalid). This closes the verifier's surviving mutation.Original controls (unchanged):
touch <canary>recipe and inspecting a refusedsh -crecipe run onlygit; the canary file is never created..hack/toolchain/run.shfails the repo-catalog check.provenance.source: "log"isuntrusted_provenance;sh -c …and the Actions-style line with repository provenance areinline_command. No argv or canary text appears in CLI stdout or stderr.stagingrecipe is refused under base and passes with--env staging; a base recipe is refused understaging.docker_socketplus an unknown token is refused withmissing_capabilities: ["docker_socket"]andunknown_capabilities: 1.Remaining gaps (host-bound or owned elsewhere)
script.sh -c).reviewedmeans the pins match. It does not prove who reviewed the recipe, so execution must not treat it as authorization.bun run testsuite and hosted CI were not run locally.Rework 2
Addresses the independent verifier's round-1 findings. Commits on top of
267f62be:356f6e3b(fix),aab02f85(gitleaks allowlist for the existing synthetic canary),2dd831e1(docs/env.md rows), and merges of the current basewf/f17-ax-contracts-b16(a3eb34e9, then5352415d)..devproject directory (blocking). Machine-local entries are now excluded after either project directory name,.hackor the legacy.dev(fromHACK_PROJECT_DIR_PRIMARY/HACK_PROJECT_DIR_LEGACY)..dev/.internal/state.json,.dev/hack.env.local.yaml,.dev/hack.env.prod.local.yaml,.dev/tickets/x.mdandapps/web/.dev/.internal/xarerecipe_invalidin any letter case;.dev/docker-compose.ymland.dev/tasks.jsonstay accepted.git --literal-pathspecs -C <root> -c core.fsmonitor=false ls-files -z --cached -- <pins>runs per pinned recipe, after the lexical and symlink checks and before any pinned byte is read. An untracked or gitignored file (.npmrc,.netrc,secrets.json, ...), a file removed from the index, or a checkout without Git givesinput_path_invalidwith no digest. The docs no longer claim the denylist alone keeps value-derived digests out of output. Known gap, documented: the digest is of the working-tree bytes, so an uncommitted local edit to a tracked file can still appear as a digest in a stale refusal.npx -c/--call,npm exec -c,pnpm -c/--shell-mode,yarn exec,bun exec,nodemon --exec,tsx/ts-node(-e,-p,-i, no operand),sudo -s|-i,doas -s,git -c/--config-env/--exec/-x/--extcmd/submodule foreach,nix --expr/eval/repl/develop/shell,make --eval/-E/-f -,deno run -,elixir/mix -e,erl -eval,entr -s,parallel/sgwith a command, andrunuser,nix-shell,concurrently,watchexec,tmux,iex, vi/Vim/Neovim and Emacs at any position. Every repro in the verifier's list is nowinline_command. The docs now call the check a denylist of known programs that is fail-closed only for the programs it knows.gitcall and a recordedls-files, so a recorder that fails to load cannot pass.wf/f17-ax-contracts-b16is still based on3b530068, behind currentnext; that belongs to feat(agents): shared AX target and result contract with a read-only inspector #346.Rework 2 validation (local, macOS, Bun 1.4.3; package.json pins 1.4.2)
Head
6400eec4:bun test tests/project-task-catalog.test.ts tests/project-target-inspect.test.ts: 40 pass, 0 fail.bun run test: 7253 pass, 126 skip, 8 fail, 7387 tests across 524 files (1h16m, host load average 12-18). Every failure is in code this stack does not touch. Rerun serially with--timeout 60000:agent-onboard-command,init-assistant,native-compose-file-commandandnative-env-valuespass. Three native census tests still fail on deadline errors ("Native process census expired", "live-session observation refused (unknown)"): one innative-authored-process-incarnationand two innative-authored-project-recovery. I treat these as load-only and have not masked them; hosted CI is the clean signal.bun run --cwd packages/cli typecheck: 0 errors.bun run --cwd packages/cli check: 0 errors (34 existing warnings). Rootbun run typecheckandbun run check: pass. Biome on the changed files: clean.gitleaks git --config .gitleaks.toml --redact --log-opts='origin/next..HEAD'(8.30.1): no leaks.Mutations, each killed by the named test:
untracked-secret, registrationls-filesassertion, domain tracking test)..hack: the path grammar test fails.npx,gitforeach,sudo,ts-nodeorrunuserfrom the tables: the inline grammar test fails for each.Hosted CI: CI run 39441252597 (pull_request) was queued at 2026-10-20T14:39Z for head
6400eec4. Earlier runs on this branch took 3 to 4 hours, so the result was not available when this section was written.Rework 2, round 2 (head
14676d5a)Re-checked the verifier's round-1 findings against the remote head
6400eec4. All four in-scope items were already fixed there by356f6e3b; each was re-proved by a mutation on the new head:.devlocal state (blocking): fixed.isSafeInputPathexcludes machine-local entries after.hackor.dev. Mutation: dropHACK_PROJECT_DIR_LEGACYfrom the project-dir set → the path grammar test fails.git ls-filesbefore any byte is read). Mutation: make the tracked check always true → 3 tests fail (paireduntracked-secretfixture, registrationls-filesassertion, domain tracking test).inline_command.gitcall andls-files.next(informational): resolved. feat(agents): shared AX target and result contract with a read-only inspector #346 was squash-merged intonext(8c790004) and this PR showedCONFLICTING. Mergedorigin/next(c7336989). Every conflict was additive (the task command, the MCP tools, and the docs, CLI reference and env-matrix rows); the merged tree differs fromnextonly in the 14 F18 files.New commits:
125e2127(from an earlier unpushed rework pass, reviewed and kept): refusesdata:URL modules for Node, Bun, Deno and tsx,git bisect/filter-branch, andjulia -e/--print/no operand. Mutation of those rules → the grammar test fails.14676d5a: refusesgit fetch/clone/ls-remote --upload-packandgit push --receive-pack, because git runs both values as shell commands. Also maps a dash version suffix (node-22 -e) to the same interpreter asnode22.git push -u origin main,git fetch originandnode-22 script.jsstill pass. Each rule has its own mutation, and each mutation makes the grammar test fail.Validation (local macOS, Bun 1.4.3; package.json pins 1.4.2):
bun test tests/project-task-catalog.test.ts: 18 pass, 0 fail.bun run --cwd packages/cli typecheck/check: 0 errors (34 warnings that were already there). Rootbun run typecheck/bun run check: pass.gitleaks git --config .gitleaks.toml --redact --log-opts='origin/next..HEAD'(8.30.1): 11 commits, no leaks.bun run test: 7124 pass, 149 skip, 255 fail across 533 files (1h31m, load average 14-28). Every failing file was rerun serially with--timeout 60000. These pass:mcp,mcp-prompts,agent-plugin-generation,env-selection-parity,e2e-shard,mcp-publication-readiness,env-command-matrix, the task catalog and target inspector suites, and 38 other files. Nine native files still fail:native-authored-project-start,native-authored-process-incarnation,native-authored-project-recovery,native-compose-adoption-generation,native-compose-file-command,native-compose-proxy-probe,native-compose-retained-storage-command,native-compose-storage-activation-commandandnative-compose-storage-fence-cost. Run serially on a plain export oforigin/nexton the same host, these same files also fail with the same errors: census expiry,engine:false, and "Completed-job worktree acceptance refused".native-authored-project-startpassed once onnextand failed on the next run. These failures come from the host and its load, not from F18. They are not masked. Hosted CI is the clean signal;6400eec4passed every hosted check.Not fixed here (needs a product decision): SQL clients that take a statement string (
psql -c,sqlite3 db "…") andpython -m codestill pass.psql's\!andsqlite3's.shellcan reach a shell. Refusing everypsql -cwould also refuse common reviewed database tasks.🤖 Generated with Claude Code