feat(antigravity): strict hook-guard gate for the headless agy CLI - #3293
feat(antigravity): strict hook-guard gate for the headless agy CLI#3293logan683 wants to merge 14 commits into
Conversation
…se hooks
The generated hook resolved its command via shutil.which("graphify"), which
returns the console-script shim. For uv-tool and pipx installs that shim is
unsigned; on Windows with Smart App Control enabled, code-integrity policy
refuses to load it, so the hook can never run. Both ~/.local/bin/graphify.exe
and the venv Scripts/graphify.exe are NotSigned, so resolving to a different
shim directory does not help.
The hook also had no failure guard. A PreToolUse hook that exits non-zero
blocks the tool call it was meant to advise, so an advisory graph check could
break the host on any launcher failure.
Resolve to '<sys.executable> -m graphify' via the existing hooks._pinned_python(),
the same reasoning already applied to git hooks, and make both the POSIX and
Windows commands fail open with a timeout.
The Graphify-Labs#2165 regression parses the subcommand out of the hook command; it now
handles both supported grammars rather than assuming the subcommand is the
second token, so it keeps proving the same contract.
Refs Graphify-Labs#3280
The generated Windows command may be run by cmd.exe or by PowerShell, and no bare fail-open suffix is correct in both. `& exit /b 0` is cmd syntax; run by PowerShell the bare `&` is the background operator, so the hook spawns a job and exits 1 - the fail-open suffix caused the very failure it was added to prevent (Graphify-Labs#3280). `; exit 0` has the mirror problem: valid in PowerShell, rejected by cmd.exe. Naming the interpreter removes the ambiguity. Measured on Windows, the generated string now exits 0 under both pwsh and cmd.exe; the previous form exited 1 under pwsh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPaGbwQeyWLwbVm9uRgHtU
Strict Claude hooks recorded no query evidence after the graph-wide stamp
was removed: the Read deny fired once per session unconditionally and the
retry was unconditional, and Bash/Grep search was nudge-only by design. An
agent instructed to read and search through Bash never met a block, and a
nudge that costs nothing is learned in one turn to cost nothing.
- PostToolUse `hook-guard mark-queried` writes cache/hook_sessions/<sid>
[--agent-<id>].queried after mcp__graphify__{query_graph,get_node,
get_neighbors,get_community,god_nodes,shortest_path} or a Bash command
that runs `-m graphify query|explain|path` (project sidecar form included).
- `hook-guard search --strict` denies a recursive in-project corpus search
(grep -r/-R, rg, find, fd, ack, ag, or the Grep tool on a directory) until
that marker exists. Not once-per-session: the way out is a query, not a
retry. Exact-file grep, stdin grep, git grep, out-of-project targets, Glob,
soft mode, no session_id, malformed stdin and GRAPHIFY_HOOK_STRICT=0 never
block. Deny text is constant; parsing never executes the command.
- Quoted spans are protected before the operator split so a regex with
parens or a "$(cat graphify-out/.graphify_python)" interpreter cannot
fragment its segment.
- install: --strict now applies to Bash|Grep; the PostToolUse marker hook is
registered and removed with the others.
Tests: tests/test_search_strict.py (RED on 2a83725: 12 failed / 5 passed;
GREEN: 301 passed across the hook/install suites), skillgen --check 134 OK,
ruff clean. Live proof: a fresh `claude -p --model claude-opus-5` session
in a scratch project with the candidate hooks had its second tool call
(grep -rn ... .) denied, queried the graph next, and was then allowed.
Stacked on Graphify-Labs#3281 (pr/hook-fail-open); follow-up to Graphify-Labs#3280.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNTa1HY67hwopSPQtffAPy
Round 1 review findings on the strict search gate: - F1: a Bash-only session never reached the 24-hour marker sweep that lived only inside _mark_session_denied, so .queried markers accumulated and a reused session id stayed pre-authorized. Both writers now call one shared _gc_session_markers. - F2: Claude Code on Windows exposes a PowerShell tool (tool_name "PowerShell", tool_input.command; PreToolUse:PowerShell hook events were measured in a live transcript). Its recursive in-project searches now reach the same guard: matcher Bash|Grep|PowerShell, and two measured shapes are denied until query evidence exists — `Get-ChildItem|gci|ls|dir ... -Recurse ... | Select-String|sls` and `Select-String ... -Path <dir or dir\*>`. Exact-file -Path, Get-Content | Select-String and out-of-project targets only nudge; non-search PowerShell stays silent. Tests: 5 RED on a0a4f1d, 306 passed after; skillgen --check 134 OK; ruff clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNTa1HY67hwopSPQtffAPy
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNTa1HY67hwopSPQtffAPy
…ll listing Round 3 review findings: - F1: _session_has_queried now enforces the 24-hour TTL itself and best-effort unlinks an expired marker, so a stale .queried never pre-authorizes a reused session id while no writer happens to sweep. One _SESSION_MARKER_TTL shared with the GC. - F2: a recursive PowerShell listing without a Select-String pipe (Get-ChildItem|gci|ls|dir ... -Recurse) is the same corpus-enumeration class as `find .` and now denies in-project, nudges out-of-project, and stays silent when not recursive. Tests: 2 RED on 8d04a72, 326 passed after; skillgen --check 134 OK; ruff clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNTa1HY67hwopSPQtffAPy
Measured on Google Antigravity CLI (agy -p): global rules load but are advice only — claude-sonnet-4-6 and gpt-oss-120b both grep the tree first with an always_on graph rule active — workspace .agents/rules never load headless, and hook payloads carry workspacePaths: []. `graphify antigravity install` therefore changed nothing for the CLI. - `hook-guard agy`: PreToolUse + PreInvocation handler for agy's camelCase payload. call_mcp_tool -> graphify writes a per-conversation marker (~/.graphify/agy_sessions, TTL shared with the Claude gate); grep_search, find_by_name, list_dir, or run_command running a recursive search (including `powershell -Command "..."` payloads) are denied until it exists. Workspace = workspacePaths[0] when sent, else the call's own target path walked up to graphify-out/graph.json; never cwd (agy runs hooks from the hooks.json directory). PreInvocation injects the same instruction while the workspace is known and unqueried. Fails open. - `graphify antigravity install --strict` merges a `graphify-graph-first` entry into ~/.gemini/config/hooks.json using the pinned interpreter in module form (a quoted script path after -File breaks under agy's cmd /c); `antigravity uninstall` removes only that entry. - `_ps_recursive_search_targets`: `-Path dir\*.md` names the directory. Tests: tests/test_hook_agy.py (11; RED on 968715a, GREEN after), 248 passed across the hook/install suites; skillgen --check 134 OK; ruff clean. Live: the PowerShell twin of this gate, installed on the author's machine, forced both models to query first (conversations 4a618532, 1df5adbc). Stacked on Graphify-Labs#3291. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNTa1HY67hwopSPQtffAPy
…separator On POSIX a literal backslash is part of a file name, so the fabricated "<ws>\*.md" climbed to the pytest dir and the gate correctly allowed; CI on Linux failed while Windows passed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HNTa1HY67hwopSPQtffAPy
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_claude\_pretooluse\_hooks behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"strict":"True","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar… but the new code produces \[\{'matcher': 'Bash\|Grep\|PowerShell', 'hooks': \[\{'type': 'command', 'command': '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify hook\-guard search \-\-strict \|\| true', 'commandWindows…. Paste that input straight into a regression test.
Behavior changes: \_resolve\_graphify\_exe changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_resolve\_graphify\_exe behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"project":"True"\}, the old code produced 'graphify' but the new code produces '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify'. Paste that input straight into a regression test.
Graphify review — findings
Adds a graphify antigravity install --strict mode that registers an agy hook-guard gate in ~/.gemini/config/hooks.json (PreToolUse + PreInvocation), deriving the graph root from each call's own target path—since the headless Antigravity CLI loads rules as advice only and sends empty workspace paths—and denying grep_search, find_by_name, list_dir, and recursive run_command searches until the conversation has queried the graph. Extends strict Claude hooks to block a first recursive in-project search (grep -r, rg, find, the Grep tool on a directory, or PowerShell Get-ChildItem -Recurse | Select-String) until the session records one graph traversal via a PostToolUse mark-queried marker keyed per session_id and agent_id; both marker writers GC entries older than 24h, and exact-file grep, stdin grep, git grep, out-of-project paths, Glob, and soft mode never block. Fixes the per-agent guard leak where one graph-wide 30-minute stamp let a parent's first read disable every subagent's block, and reroutes generated agent guidance and project hooks to prefer the MCP query_graph tool with the graph's recorded interpreter (graphify-out/.graphify_python) as CLI fallback, failing open when that sidecar is absent so hardened Windows hosts avoid the unsigned graphify.exe shim.
Worth a look
- Command substitution inside double quotes bypasses search detection —
graphify/cli.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Always-on instructions execute an interpreter path read from the workspace —
graphify/always_on/gemini-md.md:6· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Always-on instructions execute an interpreter path read from the workspace —
graphify/always_on/kiro-steering.md:5· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Always-on instructions execute an interpreter path read from the workspace —
graphify/always_on/vscode-instructions.md:5· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Double-quoted command substitutions are ignored —
graphify/cli.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2343 functions depend on the 2165 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 122 callees - new:
codebuddy_install()— 20 callers, 5 callees - new:
_run_hook_guard()— 6 callers, 15 callees - new:
claude_install()— 19 callers, 4 callees - new:
gemini_install()— 10 callers, 7 callees - new:
claude_uninstall()— 17 callers, 4 callees - new:
_project_uninstall()— 5 callers, 13 callees - new:
render()— 13 callers, 5 callees - …and 23 more — each is listed as a finding
Verification — 2343 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 2289 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_claude\_pretooluse\_hooks behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"strict":"True","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar… but the new code produces \[\{'matcher': 'Bash\|Grep\|PowerShell', 'hooks': \[\{'type': 'command', 'command': '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify hook\-guard search \-\-strict \|\| true', 'commandWindows…. Paste that input straight into a regression test.
Behavior changes: \_resolve\_graphify\_exe changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_resolve\_graphify\_exe behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"project":"True"\}, the old code produced 'graphify' but the new code produces '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify'. Paste that input straight into a regression test.
No difference found (not proven): No behavior difference found in \_bash\_invokes\_search (not a proof).
The verifier ran both versions of \_bash\_invokes\_search on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in \_mark\_session\_denied (not a proof).
The verifier ran both versions of \_mark\_session\_denied on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in \_run\_hook\_guard (not a proof).
The verifier ran both versions of \_run\_hook\_guard on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_antigravity\_install.
The verifier did not have enough to check \_antigravity\_install, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `project_dir` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_antigravity\_uninstall.
The verifier did not have enough to check \_antigravity\_uninstall, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `project_dir` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify claude\_install.
The verifier did not have enough to check claude\_install, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 30 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in dispatch\_install\_cli (not a proof).
The verifier ran both versions of dispatch\_install\_cli on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_install\_claude\_hook.
The verifier did not have enough to check \_install\_claude\_hook, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `project_dir` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_install\_codebuddy\_hook.
The verifier did not have enough to check \_install\_codebuddy\_hook, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `project_dir` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_install\_codex\_hook.
The verifier did not have enough to check \_install\_codex\_hook, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `project_dir` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_print\_install\_usage (not a proof).
The verifier ran both versions of \_print\_install\_usage on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_strip\_graphify\_hook.
The verifier did not have enough to check \_strip\_graphify\_hook, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `settings_path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_uninstall\_codebuddy\_hook.
The verifier did not have enough to check \_uninstall\_codebuddy\_hook, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `project_dir` is annotated `Path` — outside the synthesizable primitive/collection set
· 7 grounded finding(s) anchored inline below; 24 more finding(s) on lines outside this diff (see the check run).
| return None | ||
|
|
||
|
|
||
| def _run_hook_guard_agy() -> None: |
There was a problem hiding this comment.
_run_hook_guard_agy()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| out({"decision": "allow"}) | ||
|
|
||
|
|
||
| def _run_hook_guard(kind: str, strict: bool = False) -> None: |
There was a problem hiding this comment.
_run_hook_guard()
fans out to 15 callees (efferent coupling); 6 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| print(f" {path} -> {_ANTIGRAVITY_HOOK_KEY} hook removed") | ||
|
|
||
|
|
||
| def _antigravity_install(project_dir: Path, strict: bool = False) -> None: |
There was a problem hiding this comment.
_antigravity_install()
high coupling complexity (Ca·Ce = 16).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| 'powershell.exe -NoProfile -NonInteractive -Command ' | ||
| f'"{inner}; exit 0"' | ||
| ) | ||
| def _install_codex_hook(project_dir: Path, project: bool = False) -> None: |
There was a problem hiding this comment.
_install_codex_hook()
7 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| print("Strict mode: the first raw file read per session is blocked until one") | ||
| print("graph traversal runs, and recursive in-project searches stay blocked until") | ||
| print("this session has run one (toggle with GRAPHIFY_HOOK_STRICT=0).") | ||
| def _install_claude_hook(project_dir: Path, strict: bool = False, project: bool = False) -> None: |
There was a problem hiding this comment.
_install_claude_hook()
fans out to 6 callees (efferent coupling); 7 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8") | ||
| print(f" .claude/{settings_path.name} -> PreToolUse hook removed") | ||
| print(f" .claude/{settings_path.name} -> graphify hooks removed") | ||
| def uninstall_all(project_dir: Path | None = None, purge: bool = False) -> None: |
There was a problem hiding this comment.
uninstall_all()
fans out to 13 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert _is_deny(_invoke("search", _search("grep -rn foo ."), tmp_path, monkeypatch)) | ||
|
|
||
|
|
||
| def test_allows_after_mcp_query(tmp_path, monkeypatch): |
There was a problem hiding this comment.
test_allows_after_mcp_query()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Stacked on #3291 (which is stacked on #3281); diff against
v8includes both until they merge.Problem
graphify antigravity installwrites.agents/rules/graphify.mdand a workflow. Measured on the Antigravity CLI (agy -p, headless):.agents/rulesandAGENTS.mdare never loaded (probe rules with unique tokens in a plain and agit initworkspace returned nothing; the same rules under~/.gemini/config/returned their tokens);claude-sonnet-4-6andgpt-oss-120b-mediumboth madegrep_searchtheir first call on a neutral question, in three rule placements;workspacePaths: [], and hooks run from thehooks.jsondirectory, so a gate keyed on workspace or cwd sees nothing.Fix
hook-guard agy— PreToolUse + PreInvocation handler for agy's camelCase payload.call_mcp_tool→graphifywrites a per-conversation marker (~/.graphify/agy_sessions, same TTL and identity hashing as the Claude gate).grep_search,find_by_name,list_dir, orrun_commandrunning a recursive search (grep -r,rg,find,Get-ChildItem -Recurse,Select-String -Path <dir\*>, including insidepowershell -Command "…") are denied with a reason naming the exact call until the marker exists. Workspace =workspacePaths[0]when sent, else the call's own target path walked up tographify-out/graph.json.view_file, bounded searches, other MCP servers, no conversationId, no graph, malformed stdin → allow. Fails open.graphify antigravity install --strictmerges agraphify-graph-firstentry into~/.gemini/config/hooks.json(module form of the pinned interpreter — a quoted script path after-Filebreaks under agy'scmd /claunch);antigravity uninstallremoves only that entry.Evidence
tests/test_hook_agy.py: 11 cases — RED on968715a, GREEN after; 248 passed across the hook/install suites;tools.skillgen --check134 OK;ruffclean.gpt-oss-120b-medium(conv4a618532)find_by_namedenied →call_mcp_tool graphify query_graph→ allowed;claude-sonnet-4-6(conv1df5adbc)grep_searchdenied → "The graph-first rule requires a graphify query first. Let me comply" → query → allowed. Before the gate, eight runs across three rule placements all grepped first.Not covered
workspacePathsis presumably populated there; the gate uses it when present, unmeasured.🤖 Generated with Claude Code
https://claude.ai/code/session_01HNTa1HY67hwopSPQtffAPy