Skip to content

feat(opencode): home-manager-managed opencode config β€” generated AGENTS.md, env plugin, permissions, 3 subagents - #276

Merged
ZacxDev merged 5 commits into
mainfrom
feat/opencode-nix-config
Aug 2, 2026
Merged

feat(opencode): home-manager-managed opencode config β€” generated AGENTS.md, env plugin, permissions, 3 subagents#276
ZacxDev merged 5 commits into
mainfrom
feat/opencode-nix-config

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Brings opencode under home-manager, the way ~/.claude/ already is. Measured against opencode v1.18.4 on this host; no home-manager switch was run and the live ~/.config/opencode/ was never modified.


πŸ”΄ 2bacd5f β€” the glob approach is replaced by a parser

Everything below the next horizontal rule describes the glob permission system and the two rounds of patching it. That approach has now been abandoned as the primary control, and this section says why and what replaced it. The rest of the PR body (generated AGENTS.md, the env.js correction, cost measurements, subagents) is unaffected and still stands.

The measurement that ended it

Two rounds of pattern-patching each closed the spellings we thought of and left the ones we did not. Replaying c1e4c02's own config through the resolver model, on the primary agent:

Command Resolved at c1e4c02 Now
talosctl -n 192.168.50.94 reset allow β€” a node wipe, no prompt deny
talosctl --nodes 192.168.50.94 reset allow deny
talosctl --nodes=192.168.50.94 reset allow deny
talosctl -e 1.2.3.4 -n 5.6.7.8 reset allow deny
rm -f -r / allow deny
rm --recursive --force / allow deny
mke2fs /dev/sdc allow deny
mkswap /dev/sdd allow deny
git -C /tmp/x reset --hard allow (on review) deny

The ask block required the tool and the verb to be adjacent ("*kubectl delete*"), so any interleaved flag walked past it. The deny block had been given infix wildcards in the previous round; the ask block had not. "*rm -rf*" and "*mkfs*" each knew exactly one flag order and one binary name.

And on the review agent, git -C * diff* sits after its own "*": deny, and opencode's * is an unrestricted dotAll .* that crosses spaces β€” so the word "diff" anywhere in the argument text satisfied it. Verified by execution:

git -C <path> stash push -m 'wip on the diff'   -> RAN, and created a stash
git -C <path> stash push -m 'wip'               -> denied

A glob over full command text cannot express "this command wipes a node" β€” the set of spellings is unbounded. So the hard denies moved to something that parses.

The architecture

Layer 1 β€” opencode.jsonc globs: FRICTION. Broad ask on mutation families so a human sees the command. Now infix-tolerant (*kubectl*delete*), deliberately over-matching, and honest about being non-airtight. Shrunk from 73 rules to 64, and the header no longer claims the patterns catch everything.

Layer 2 β€” scripts/opencode/plugin/guard.js β†’ scripts/claude-hooks/guard_core.py: ENFORCEMENT. Splits on ;/&&/||/|/&, strips VAR=… prefixes and sudo/doas/env/timeout/nice/… wrappers, recurses into bash -c '…' and eval, scans $( )/backtick bodies, and reasons about argv. Throws from tool.execute.before, which hard-blocks the call. ~26 ms per bash call.

One implementation, two harnesses, two named policy sets β€” guard_core.py is deployed by home-manager to both ~/.claude/hooks/guard_core.py (imported by bash-guard.py) and ~/.config/opencode/guard_core.py (resolved by the plugin relative to its own module URL).

Which hook β€” and whether ask is expressible

Measured on 1.18.4, this host, 2026-08-02, with a probe plugin in a sandbox OPENCODE_CONFIG_DIR:

  • permission.ask is in the Hooks type and its output.status is typed "ask" | "deny" | "allow", so returning an ask decision looks expressible. It is not. The hook never fired in any probe β€” not on the allow path, and not on the ask path either (an ask rule under opencode run printed auto-rejecting without the hook logging a single line). A hook that does not run cannot upgrade an allow into an ask, which is the one thing a guard would need it for.
  • tool.execute.before fired on every bash call, and throwing from it hard-blocks: opencode reports the thrown message to the model as a tool error and the command never runs. (The probe model then tried printf, type echo and a PROBE_THROW='' … re-spelling β€” every matching variant was blocked.)

So DENY is expressible from a plugin and ASK is not. Ask-grade families stay as globs, per the brief: globs are acceptable for friction, unacceptable as the only thing between the agent and an irreversible action.

Two further measurements worth recording: opencode run AUTO-REJECTS an ask (it prints auto-rejecting) β€” only the interactive TUI turns one into a prompt, while opencode debug agent --tool auto-approves it. So ask is friction for a human, never a control on an unattended agent. And tool.execute.before fires before the permission check.

Not verified: whether permission.ask fires in the interactive TUI. Driving the TUI was out of reach here, so the claim is scoped to headless opencode run and debug agent --tool.

πŸ”΄ Claude Code's behaviour is unchanged β€” and here is the proof

bash-guard.py becomes a thin adapter running the claude-code policy, which is frozen at the original six checks. It fires on every Bash call in every Claude Code session on both hosts, so this was treated as untouchable.

Evidence Result
Existing test_bash_guard.py (unmodified) 113 PASS, 0 FAIL
Before/after decision matrix, 2,097-command corpus, OLD(c1e4c02) vs NEW 0 rows differ
Positive control: OLD vs OLD 0 diffs
Negative control: OLD vs OLD-with-check_git_add_all-removed 324 diffs β€” the harness can see a change
test_claude_code_policy_is_frozen_at_the_original_six pins the six by name against a literal list, so a check added to the shared core cannot leak in
test_the_new_checks_do_not_leak_into_claude_code pins the same thing by outcome for each new rule

The corpus is an outer product of blind/safe pathspecs Γ— git global-option hops Γ— wrapper prefixes, plus every new-check family across nine prefixes and every separator.

New checks β€” opencode policy ONLY, none reach Claude Code

  • talosctl reset (any argv position β€” -n <ip>, --nodes, --nodes=, --talosconfig … -n … reset)
  • mkfs / mkfs.* / mke2fs / mkswap / newfs / mkntfs / mkdosfs
  • dd of=<block device> (/dev/null, /dev/zero, /dev/std*, /dev/fd/N remain allowed; reading a device with if= is allowed)
  • rm -r of /, /*, ~, $HOME, ., .., or a top-level system dir
  • git stash (list and show stay allowed β€” RULES calls stash list the safe diagnostic)
  • git clean -f
  • git reset --hard through a -C hop

πŸ”΄ One thing for you to veto or approve

git -C <path> reset --hard is a live gap in Claude Code right now. The frozen check_git_reset_hard is a raw-text regex anchored on \bgit\s+reset\b, so the worktree-first spelling RULES.md mandates does not match it. Measured: it passed the Claude Code guard at c1e4c02 and still does.

check_git_reset_hard_argv closes it and is enabled for opencode only. Turning it on for Claude Code is a one-line change (_CLAUDE_CODE_CHECKS.append(check_git_reset_hard_argv)) and is deliberately left to you, because it is a new deny on your primary tool. It is the only item on this list I would actually recommend adopting.

One deliberate loosening

The old glob "*rm -rf /*" also denied rm -rf /tmp/scratch, because * crosses /. With a parser the fatal set is exact, so an ordinary absolute-path cleanup now resolves to the broad rm -r ask instead of a deny. Recorded in check_rm_rf_critical's docstring so it is a decision rather than an accident.

The other audit πŸ”΄s

  • review.md β€” the writing verbs (stash, commit, push, reset, clean, checkout, restore, rebase, add -A, branch -D, worktree add|remove, rm , mv , >) are re-stated as denies after the allow-list, so last-match-wins makes them win. Scoped rather than blanket: an unscoped *merge*/*branch*/*add* would also block git log --merges, --branches and -S'add', and a reviewer that cannot read history invents findings. The agent prompt now tells the reviewer this over-block exists and not to route around it.
  • browser-agent:136-137 β€” the command -v opencode preflight is hoisted above the bootstrap. Without opencode on $PATH, readlink -f resolved a bare string against cwd, the stamp could never match, oc_is_warm always failed, and _oc_bootstrap_locked took its "stale tree" branch and rm -rf'd node_modules out of the shared cache on every run β€” then failed to re-warm, leaving the dir cold. Reachable from systemd/cron/a switch.
  • test_opencode_config.py:93 β€” pytest.importorskip("yaml") β†’ a hard import yaml, and the false comment above it is replaced with what was actually measured (486 passed, 1 skipped, exit 0, with 384 assertions silently gone).
  • Lock wait vs. the work it guards β€” was a fixed 30 s around a 90 s warm, so the loser force-stole the lock and rm -rf'd node_modules while the first process was still writing. Now derived from OC_WARM_TIMEOUT + 30 s.
  • Interrupted warm stamped valid β€” [ -d node_modules ] alone is not completeness. Now requires the timeout not to have fired (rc != 124) and node_modules/@opencode-ai/plugin to resolve.
  • browser-agent tests were substring greps β€” they now run against comment-stripped source, with a negative control proving the stripper works. One assertion ("--version" in src) was being satisfied by a comment saying the code deliberately does not use --version. Added: preflight-ordering, config-dir-is-not-the-live-one, complete-warm, and lock-wait tests.

Test discipline

Red/green. The new tests were run against a pre-change seed tree (c1e4c02's opencode.jsonc + review.md + browser-agent, guard degraded to the six): 175 failing there, 1,171 passing at HEAD.

That run earned its keep immediately β€” it caught a vacuous assertion in one of the new tests. test_browser_agent_checks_for_opencode_before_touching_the_cache matched the bare substring command -v "$OPENCODE_BIN", which also matches the copy inside oc_warm_version() at line ~136 β€” before the bootstrap even at c1e4c02. It passed against the exact ordering bug it was written to catch. It is now anchored on the || die preflight and is red on the seed.

Multi-spelling matrices, not one spelling per rule. The glob-era suite pinned one spelling per pattern β€” always the one the pattern was written around β€” which is precisely why it was blind. Every new rule is now exercised across an outer product of nine prefixes (bare, VAR=, sudo, sudo -n, doas, env VAR=, timeout N, nohup, KUBECONFIG=… sudo), each git global-option hop (-C, doubled -C, --git-dir=, --no-pager, -c k=v, -P), and each of the five separators plus bash -c / eval / $( ).

That matrix found a real bug in the first draft of the parser: sudo -n <cmd> peeled to the wrong argv, because -n had been put in a shared value-flag set for nice -n 5, so sudo -n talosctl reset became ['reset'] and the node-wipe guard silently stopped firing on the most ordinary sudo spelling there is. Fixed with per-wrapper arity tables plus _peel_variants, which branches on the other arity interpretation so a future table error cannot fail open.

Mutation-tested by an independently-constructed sweep (fe2e56e). 52 mutants, built deliberately unlike the suite they attack β€” pattern-narrowing mutants that keep the function, the policy entry and the deny action and change only what matches, plus parser, policy and off-by-one mutants. 39/50 killed, 11 survived, every kill by assertion (test counts were checked per run β€” no truncation, no collection errors scored as kills).

The sweep's own harness was broken on its first run: a helper-argument bug made all 52 "mutants" byte-identical to pristine, and the pre-flight "all mutants apply and parse" check passed because nothing was applied. Only the known-bad control caught it. Both controls now behave: the obvious mutant is killed (49 failures), the byte-identical control survives with counts matching pristine exactly.

All 11 survivors were holes in the test suite, not defects in guard_core.py β€” the code already handled each case; nothing pinned it. Closed in fe2e56e:

Survivor Gap Why it mattered
N16 / P14 no test used an absolute argv[0] or wrapper path on NixOS /run/current-system/sw/bin/talosctl and /run/wrappers/bin/sudo are the normal spellings
P04 _peel_variants' arity branching was pure prose it is the safety net the docstring says exists so a wrong flag table cannot fail open β€” the exact sudo -n class of bug
P13 bundled -c shells (bash -xc '…') only bare -c was pinned
S08 the unlexable-input fallback in _tokenise untested for every check, and reachable from any unbalanced quote
S09 cd <path> **;** git … only the && spelling was pinned
S11 private-key headers other than RSA PRIVATE KEY i.e. the wildcard that covers modern OPENSSH keys
N14 no test pinned any individual secret shape deleting three sk-* patterns changed nothing
N15 only gh … create sinks gh pr comment/edit/review could post a secret
S12 the public-IP arm was entirely unpinned the check whose docstring says it exists because a real session leaked an ingress origin IP into a public-repo comment β€” exemptions unpinned too
S03 the -- boundary in _flags_and_operands brute-forced over 1,102 shapes and confirmed currently safety-neutral, so pinned on the helper directly rather than through an outcome that does not depend on it

Three weak kills were also strengthened: N13 (check_git_reset_hard_argv was killed by exactly one test β€” a one-test-deep guard on its own headline case) and S01/S04 (killed only by over-block assertions, so relaxing those would silently un-cover them).

And the new coverage found one real code bug. The generated (wrapper, value-flag) matrix went red immediately on timeout -k talosctl reset and chrt -p talosctl reset: for a wrapper that takes positionals, the alternate arity branch still hit the positional consumption and swallowed the real command, so both peelings produced ['reset']. _peel_variants now branches on positional consumption too. Same class as the sudo -n bug, invisible for the same reason.

Claude Code was re-verified after that parser change: the 2,097-command matrix still differs on 0 rows and test_bash_guard.py is still 113/113.

Invariant guards are labelled as such (test_browser_agent_config_dir_is_not_the_live_one, test_browser_agent_warm_handles_races_staleness_and_failure, the harness negative controls) and are not counted as regression coverage.

Full hermetic suite: 4,230 passed, 1 skipped, 0 errors (scripts/run-tests.sh under nix-shell -p 'python312.withPackages(p:[p.pytest p.pyyaml p.psycopg2 p.requests p.minio])'). Test counts were read per-suite, not from an exit code. Note: scripts/mail-actions/tests and scripts/initiatives/tests fail only when psycopg2/minio are absent from the shell β€” with them present both are green.

Live end-to-end verification

Against a sandbox OPENCODE_CONFIG_DIR laid out exactly as home-manager deploys it (plugin/guard.js + ../guard_core.py), with the sandbox config set to bash {"*": "allow"} so only the guard could refuse anything:

Probe Result
echo guard-negative-control (negative control) ran
mkswap /dev/zzz-nonexistent-device BLOCKED with the guard's message
talosctl --talosconfig /nonexistent-zzz -n 1.2.3.4 reset BLOCKED β€” the flag-interleaved spelling that was allow at c1e4c02
echo hello with DEVRC_GUARD_CORE=/nonexistent/guard_core.py refused β€” fail-closed confirmed

Harmless targets were chosen deliberately, so a guard failure could not have caused damage.

The review fix, verified through the task tool

opencode run --agent review silently falls back to build (review is a subagent), which invalidated an earlier probe matrix β€” so this was delegated properly via the task tool, in a throwaway git repo with a dirty tree, and with DEVRC_GUARD_DISABLE=1 so layer 1 (the globs) was tested in isolation:

Probe Result What it proves
ls -la denied we are genuinely in review β€” under the build fallback this runs. The discriminating negative control.
git -C <repo> log --oneline -3 ran (a24edc0 initial) the git -C <path> … allow-list still works; the agent is not over-blocked into uselessness
git -C <repo> stash push -m "wip on the diff" denied the exact command that executed and created a stash at c1e4c02

git stash list in that repo: 0 before, 0 after.

What I could not verify

  • Whether permission.ask fires in the interactive TUI β€” only headless opencode run and debug agent --tool were probed.
  • Nothing was deployed: no home-manager switch was run, ~/.config/opencode/ and ~/.claude/ were not touched, and ~/.cache/browser-agent-opencode-config was left alone. The home-manager wiring is asserted by tests over home.nix and by checking every source = path exists, not by a switch.
  • ask outcomes rest on the ported resolver model, not on execution: ask is auto-approved under --tool and auto-rejected under opencode run, so neither mode can distinguish it from allow/deny. Test docstrings say so.
  • Coverage gaps the guard does not claim (documented in guard_core.py): ssh host talosctl reset, xargs-fed argv, and argv assembled from variables ($CMD reset). The command text does not carry those values. The broad ask globs still apply to them.

πŸ”΄ Correction β€” the original permission claims in this PR were too strong

The first version of this PR described the permission block by reading the config, not by resolving it. An adversarial audit resolved it with opencode debug agent, and three of the four load-bearing guarantees did not hold. c1e4c02 fixes them. Recording it plainly, because the wrong version read as thorough:

Claimed Actually measured
""*": "allow" is first and every deny/ask follows it, so the denies apply" True of the global block, and irrelevant on k8s β€” its agent-level bash: {"*": allow} is appended after all 30 global rules and nullified every one of them
"plan is genuinely read-only" Its built-in task: {general: deny} had been flipped to allow by the global task: allow, so plan could reach a shell by delegating to general
"the tests pin the ordering for exactly this reason" Both mutations that matter β€” a trailing "git *": "allow" and an alphabetical key sort β€” passed at 82/82
"opencode's bash tool does not source zsh startup files" False on this host. The tool shell IS zsh (ZSH_VERSION = 5.9 inside a bash tool call) and it does source .zshenv

The AGENTS.md, cost-measurement and subagent sections below were re-checked and stand.

What was wrong, and what it meant

1. The k8s agent nullified the entire global bash block. Agent rules are appended AFTER the global ones, and opencode is last-match-wins over one flat array. k8s's bash: {"*": allow} landed at index 74 β€” after all 30 global rules β€” so only its own 4 survived. On that agent, blind-staging, git stash, hard-reset, rm -rf ~…, sops -d, nixos-rebuild and home-manager switch were all plain allow.

Fix: the agent block now only ever TIGHTENS β€” no wildcard at all (the global one already keeps bash enabled), just *talosctl*: ask with *talosctl reset*: deny restated after it so last-match-wins cannot re-open it.

2. The global block re-enabled every tool on the hidden title/summary/compaction agents. Their stock tool set is empty (verified against a bare config dir: enabled=[]). Listing tools in the global permission block appended them to these agents too, giving all three bash, edit, glob, grep, read, skill, task, todowrite, webfetch, write. compaction runs automatically on every context overflow, on the cheap model, on a path nobody watches β€” so this handed it a shell and a writer, and made every title generation carry the ~3,730-token skill catalogue.

Fix: each carries an explicit "permission": {"*": "deny"}. All three now resolve enabled=[], matching the stock control.

3. Every deny/ask was bypassable by a prefix or a wrapper. opencode matches a command node's full text, so an anchored "git stash*" misses FOO=1 git stash, sudo -n git stash and git -C /tmp stash. That is acute here because the house style mandates the bypassing spelling β€” k8s.md says to write KUBECONFIG=$KC_HOMELAB kubectl …, RULES.md says to write git -C <path> ….

Fix: every dangerous pattern is leading-*. Confirmed against the real engine that * compiles to an unrestricted dotAll .* crossing spaces, / and -, so *git*stash* catches all four spellings. (&& chains and pipelines are already checked per-command, so only the prefix/wrapper case needed closing.)

4. The ordering tests were vacuous. A key-order assertion cannot catch an alphabetical sort, because "*" (0x2A) sorts to the front β€” the wildcard still looks correct while everything else reorders and a broad ask overtakes the narrow deny it should lose to.

Fix: added the missing "no allow after the wildcard" and "all asks precede all denies" assertions, and — the real protection — a faithful port of opencode's resolver (findLast over the flat array + its glob→regex) that pins the effective resolved action for a matrix of dangerous commands on every agent.

Also fixed

  • Dropped the blanket read: allow. opencode ships a built-in .env guard (*.env β†’ ask at index 25-27); the blanket allow landed at index 30 and silently defeated it on every agent. The default already allows every non-.env read.
  • Restored plan's task: {general: deny} and added write: deny.
  • Closed ~30 unlisted command families: kubectl get/describe secret (base64 = plaintext), exec/cp/edit/replace/rollout undo, flux delete, helm rollback, sops exec-env, age -d, sudo/doas as wrappers, recursive chmod/chown, dd/mkfs, mutating systemctl verbs, nix profile remove, git checkout -- /restore/branch -D/push --force, talosctl upgrade/apply-config/shutdown, and the . / $HOME delete targets.
  • review could not run git -C <path> diff β€” resolved DENY. In the worktree-first workflow RULES.md mandates, that is the only spelling a reviewer uses, so it silently fell back to read/grep and reported a review it had never performed. The git -C * <verb>* forms are now allow-listed, deliberately anchored (a leading-* allow there would punch a hole through the global deny block).
  • The browser-agent config dir existed but was EMPTY β€” byte-for-byte the cold-start this change exists to avoid. Added a warm/bootstrap step; details below.
  • env.js was a checked-in file hardcoding /home/zach/workspace/homelab-talos, with no existence guard, duplicating programs/zsh and defining a KC_PROD zsh lacked. Both are now generated from one source (nix/agent-handles.nix); KC_PROD is reconciled into zsh.

πŸ”΄ Correction: why the env.js plugin exists

The claim that "opencode's bash tool does not source zsh startup files" is false here. With the plugin absent and VITEST_MAX_WORKERS explicitly unset in the parent, a bash tool call still reported 4 β€” a value set only by .zshenv β€” and ZSH_VERSION reported 5.9.

The original negative control ("with the plugin present $KC_HOMELAB resolves; with it absent it is empty") was real but misattributed. The kubeconfigs are gitignored and absent from the checkout, so zsh's existence guard correctly declines to export KC_HOMELAB β€” while the old unguarded env.js exported it regardless. The plugin looked load-bearing because it was pointing a handle at a file that does not exist β€” precisely the "runs against no cluster while looking like it worked" failure k8s.md warns about.

The plugin is kept as belt-and-braces (it is independent of $SHELL), and now existence-guards exactly as zsh does. The "169 hand-retyped KUBECONFIG=" motivation is unaffected.


What each item does

1 β€” generated ~/.config/opencode/AGENTS.md. Built at switch time by concatenating claude/PRINCIPLES.md + claude/RULES.md + claude/opencode-addendum.md.

It has to be a concatenation: opencode does not expand @-imports in AGENTS.md/CLAUDE.md. Proven with an all-tools-denied agent, so no file read was possible β€” an imported passphrase returned NONE, the same content inline returned verbatim. ~/.claude/CLAUDE.md is ~1.5 KB of import lines, so opencode reading it would receive none of the 32 KB of rules. A project AGENTS.md also suppresses CLAUDE.md (first match wins).

Measured: 38,363 B (37.5 KB) β‰ˆ 8.9k tokens (previously stated as 38,033 B β€” stale). Tests enforce a 100 KB ceiling; a 331 KB file causes a permanent compaction loop.

2 β€” generated plugin/env.js. From nix/agent-handles.nix, the single source of truth that also generates the zsh exports. Existence-guarded. Deployed as a single file β€” the glob is {plugin,plugins}/*.{ts,js}, non-recursive, and a .mjs will not load.

3 β€” opencode.jsonc. Model + small_model; the hidden agents pinned to flash and tool-denied; plan made genuinely read-only; compaction/tool_output/watcher/subagent_depth; autoupdate: false, share: "disabled".

The bash block is ordered "*": allow β†’ every ASK β†’ every DENY. Deny-last is load-bearing: it is what lets a narrow *talosctl reset*: deny beat a broad *talosctl*: ask.

4 β€” three subagents. nav (read-only navigator, bash denied), k8s (three clusters, read-before-mutate, commit-to-trunk-is-a-deploy), review (adversarial). Deliberately only three β€” every subagent permanently enlarges the primary's task tool description on every request. (The addendum previously said "there is deliberately no third agent" while three shipped; corrected, and review is now described to the primary agent.)

There is no list tool and no websearch tool on 1.18.4; both dead keys are removed.


browser-agent: isolation was right, warming was missing

The global AGENTS.md does reach browser-agent's scratch-dir runs β€” a tool-less probe in an unrelated scratch dir quoted a passphrase planted at the end of the file.

Condition Passphrase Input tokens
Isolated config dir (control) NONE 631 / 631
Global AGENTS.md present quoted verbatim 8,343 / 8,422
+ OPENCODE_DISABLE_CLAUDE_CODE=1 quoted verbatim 8,340 β€” zero effect
+ project-local AGENTS.md both passphrases 8,408 β€” went up (concatenated)

cache.read/cache.write were 0 in every run, so it is multiplied by $STEPS (default 12): up to ~90k tokens per run.

But the dir shipped EMPTY (ls -A β†’ 0 entries), which is exactly the cold-start case. Fixed with a warm/bootstrap step:

  • πŸ”΄ Verified by outcome, not exit code. debug paths, debug config, debug skill, debug v2 and even debug agent from a plain directory all exit 0 in ~1.2 s and install nothing. Only resolving a project's .opencode/tools/ (browser.js imports @opencode-ai/plugin) triggers the 62 MB install. A warm step using a bare debug command would have looked successful and left the dir just as cold.
  • Concurrency: an atomic mkdir lock with a bounded wait. 4 simultaneous cold runs all succeed, one warms, no lock leak.
  • Staleness: stamped with the resolved binary identity (on nix the store path encodes the version). Deliberately not opencode --version β€” that is an extra process, and it lands in the test rig's invocation log.
  • Failure: degrades to the global config loudly on stderr rather than hanging. Costly but working beats broken.
  • Isolation drops autoupdate:false, share:"disabled" and the tool_output caps. Decision: re-state all three in the isolated dir (no self-update mid-run; a browser session must never be shareable; a page dump is exactly the huge tool result those caps exist for). Deliberately not copied: the permission block, agent/, plugin/ and AGENTS.md β€” those are the cost this exists to shed, and the agent denies every host tool anyway.

Measured: cold 7.0 s, warm 0.73 s, stale stamp β†’ clean re-warm 5.2 s, unwritable dir β†’ loud degrade.

The browser-agent tests now pin their own OPENCODE_CONFIG_DIR β€” without it the new bootstrap would judge the operator's real warmed cache stale and rm -rf it as a side effect of running the suite.


Verification

Suite: 82 β†’ 384 tests. 162 are red at e353841 and green at HEAD, covering every finding.

Mutation matrix (each applied to a copy, whole suite re-run, tests counted β€” never a bare exit code):

Mutation Old suite New suite
control (no mutation) 82 passed 384 passed
trailing "git *": "allow" 82 passed β€” survived RED 37 failed (test_no_allow_rule_follows_the_wildcard + effective-action)
alphabetical key sort 82 passed — survived RED 15 failed (test_all_asks_precede_all_denies; sudo -n git stash deny→ask)
re-add "*": allow to k8s n/a RED 52 failed
de-star *git*stash* β†’ git stash* n/a RED 9 failed
restore read: allow n/a RED test_no_blanket_read_allow
drop compaction's permission n/a RED test_hidden_agents_are_pinned_and_tool_less
drop plan's task deny n/a RED test_plan_agent_is_genuinely_read_only
drop review's git -C * allows n/a RED 4 failed
move a deny above the asks n/a RED 5 failed
negative control (drop *git*stash*) RED 1 RED 13

Each fails with its own assertion, not a collateral parse error (errors=0 throughout). The harness itself was validated against a known-bad state first β€” the negative control turns it red.

Real-engine probes. opencode debug agent <a> --tool bash performs a genuine permission check. All 18 deny cases pass on build, k8s and review, including every prefix/wrapper form, with 3 allow controls. Every probe is inert if executed (destructive ones are probed in echo <cmd> form, which still exercises the leading-* pattern).

F3 bypass matrix β€” before β†’ after (real engine)

Command Before After
git stash deny deny
FOO=1 git stash ALLOW deny
sudo -n git stash ALLOW deny
git -C /tmp stash ALLOW deny
git -C /tmp reset --hard ALLOW deny
FOO=1 git add -A ALLOW deny
KUBECONFIG=… talosctl reset ALLOW deny
sudo rm -rf /… ALLOW deny
KUBECONFIG=… kubectl delete pod x ALLOW ask
kubectl get secret x -o yaml ALLOW ask

Resolved enabled= for the hidden agents: title / summary / compaction β†’ [] (stock control: []; before this commit: 10 tools each).

Full repo gate (scripts/run-tests.sh): green except scripts/mail-actions/tests (3 failures, missing minio) β€” identical at e353841, pre-existing and unrelated.

What I could NOT verify

  • No home-manager switch was run. The nix now evaluates (nix eval renders the generated env.js and the zsh envExtra in full, and the generated plugin was loaded into a sandbox and confirmed to inject handles against a scrubbed environment), but activation and the resulting symlinks are unverified against a real switch.
  • ask cannot be distinguished from allow by the executing prober. In debug agent --tool (non-interactive) an ask is auto-approved β€” proven by a 3-way control (allow/ask/deny on the same pattern β†’ ALLOW/ALLOW/DENY). So the ask rows are pinned by the resolver model, validated against the real engine on the semantics that decide them, not by execution.
  • The resolver model does not implement the shell-AST split. opencode matches each command node separately and denies if any node denies; the model matches a single node. Chains are therefore safer than the model suggests, never less safe β€” but no test asserts chain behaviour.
  • No end-to-end browser-agent run (needs a live tab + token spend). The warm/bootstrap path was exercised directly; the browser tool's runtime behaviour under isolation is not re-verified here.
  • Offline warm behaviour is inferred, not re-measured β€” the 62 MB install was observed to require network on this host as-is; I did not re-test with the network down.
  • Kubeconfigs are absent from this checkout (gitignored), so KC_HOMELAB/KC_WORKBENCH/KC_PROD resolve to unset under the new existence guard. That is correct behaviour and matches zsh, but it means the guarded handles could not be observed resolving to a real file.
  • Known limitation, now documented: session "always allow" approvals are concatenated after the config ruleset, so an in-session always-allow outranks a config deny, and the auto-generated pattern is arity-derived (approving git -C /tmp stash once would install git -C *). Not fixable in config.

πŸ€– Generated with Claude Code

ZacxDev and others added 3 commits August 1, 2026 23:56
opencode was running with none of the repo's ruleset and none of the
kubeconfig handles. Both are now deployed from this repo.

AGENTS.md is GENERATED (not symlinked) by concatenating claude/PRINCIPLES.md
+ claude/RULES.md + a new claude/opencode-addendum.md at switch time. It has
to be a concatenation: opencode does NOT expand `@`-imports inside
AGENTS.md/CLAUDE.md (measured on v1.18.4 with an all-tools-denied agent, so
no file read was possible β€” an imported passphrase returned NONE, the same
content inline returned verbatim). ~/.claude/CLAUDE.md is ~1.5 KB of
@PRINCIPLES.md/@RULES.md import lines, so pointing opencode at it would have
delivered none of the 32 KB of actual rules. Generating means it can never
drift from what Claude Code reads. Measured result: 38,033 B ~= 8.8k tokens.

env.js is a `shell.env` plugin exporting HOMELAB / KC_HOMELAB / KC_WORKBENCH
/ KC_PROD. There is no `env` config key in opencode and its bash tool does
not source zsh startup files β€” a plugin hook is the only seam. It must land
directly at plugin/env.js as `.js`: the glob is `{plugin,plugins}/*.{ts,js}`,
non-recursive, and a `.mjs` will not load.

opencode.jsonc pins the model + the hidden title/summary/compaction agents to
the cheap model (`small_model` covers title generation ONLY, not compaction),
makes the built-in `plan` genuinely read-only, and carries the bash permission
block. That block's ordering is load-bearing and is the INVERSE of Claude
Code: opencode is LAST-MATCH-WINS, so `"*": "allow"` is the FIRST key and
every deny/ask follows it. Sorting those keys silently disables every deny β€”
two tests pin the ordering, and a reorder mutation is caught by those two
alone while the other 75 tests stay green.

Three subagents only β€” nav (bash DENIED, the deterministic fix for file
navigation shelling out), k8s (three clusters, read-before-mutate,
commit-to-trunk-is-a-deploy) and review (adversarial). Each additional
subagent permanently enlarges the primary agent's `task` tool description on
every request.

home.activation.opencodeDropStaleConfig backs up the pre-existing unmanaged
~/.config/opencode/opencode.jsonc before checkLinkTargets, since `force` alone
does not displace a hand-placed regular file at a managed path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… against the resolved agent dump

Three corrections, all from running `opencode debug agent` against the real
config rather than reasoning about the JSON.

1. browser-agent must not inherit the new global AGENTS.md.
   MEASURED: the global ~/.config/opencode/AGENTS.md IS injected into a run
   whose --dir is an unrelated scratch project β€” a tool-less probe in a scratch
   dir quoted a passphrase planted at the END of the file, so it arrives in
   full. Cost 8,343 input tokens vs 631 isolated = ~7.7k extra PER REQUEST,
   with cache.read/cache.write both 0, so it is multiplied by $STEPS (default
   12): up to ~90k tokens per run, for an agent whose every host tool is denied
   and which can act on none of it.

   Neither obvious mitigation works: OPENCODE_DISABLE_CLAUDE_CODE=1 had ZERO
   effect (byte-identical token count), and a project-local AGENTS.md does NOT
   suppress the global one β€” they are CONCATENATED (both passphrases returned,
   tokens went up). Relocating OPENCODE_CONFIG_DIR is what works.

   The dir is STABLE and REUSED, never mktemp'd per run: a fresh dir makes
   opencode materialise package.json + 63 MB of node_modules (2.6-4.5 s), and
   with no network it HUNG β€” killed at 120 s having produced 0 bytes, which
   trips the harness's own tool-set gate and dies with "produced NO output". A
   warm dir runs offline fine (+0.05-0.3 s). Verified safe: auth is unaffected
   (auth.json is in the DATA dir) and the gate output was BYTE-IDENTICAL
   (10,670 B, same resolved tools map, browser:true and every host tool false)
   under live vs isolated config β€” the custom `browser` tool loads from the
   scratch project's .opencode/tools/, not the config dir.

2. There is NO `list` tool on opencode 1.18.4. The resolved tool map is exactly
   {bash, edit, glob, grep, invalid, question, read, skill, task, todowrite,
   webfetch, write}. nav.md and the addendum were telling agents to use a tool
   that does not exist β€” whose likely fallback is the shell-out these agents
   exist to prevent. Replaced with `glob` on `<dir>/*`.

3. nav was not actually lean. Resolved, it carried skill/task/todowrite/webfetch
   β€” and `skill` alone injects the whole catalogue at ~3,730 tokens per request,
   on the cheap high-frequency agent. Denying them takes nav's resolved tool set
   to exactly {glob, grep, read}.

Also records what review's deny-all really does. Resolved order is global block
then agent block: [0] allow * … [31] deny * [32..36] the git/rg allows. Last
match wins, so [31] is the effective default and any non-allowlisted command
resolves deny β€” the restriction is real. But bash stays `true` in the tool map,
i.e. it is NOT pruned from the request schema. That buys safety, not tokens, and
the comment now says so rather than implying otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ective outcomes

Ten findings from the adversarial audit of #276. The permission claims in the
original PR body were too strong: the block was measured by reading the config,
not by resolving it, and three of the four load-bearing guarantees did not hold.

CRITICAL

* The `k8s` agent's `bash: {"*": allow}` nullified the ENTIRE global block.
  Agent rules are appended AFTER the global ones and opencode is
  last-match-wins, so that wildcard landed at index 74 (after all 30 global
  rules) and only its own 4 survived. Blind-staging, the stash ban, hard-reset,
  `rm -rf ~…`, `sops -d`, `nixos-rebuild` and `home-manager switch` were all
  plain ALLOW on that agent. The agent block now only TIGHTENS: no wildcard,
  just `*talosctl*: ask` with `*talosctl reset*: deny` restated after it.

* The global `permission` block re-enabled all tools on the hidden
  title/summary/compaction agents, whose stock tool set is EMPTY. compaction
  runs automatically on every context overflow, on the cheap model β€” it had
  bash and write, and every title generation carried the ~3,730-token skill
  catalogue. Each now carries `"permission": {"*": "deny"}`; all three resolve
  `enabled=[]` again, matching a stock control.

* Every deny/ask was bypassable by a prefix or wrapper. opencode matches a
  command NODE's full text, so `FOO=1 …`, `sudo -n …` and `git -C <path> …`
  all missed the anchored patterns β€” and those are exactly the spellings the
  house style mandates. Every dangerous pattern is now leading-`*`; `*` is an
  unrestricted dotAll `.*` that crosses spaces, `/` and `-`, verified against
  the real engine.

* The ordering tests were vacuous. A trailing `"git *": "allow"` and an
  alphabetical key sort both passed at 82/82. Added the missing "no allow after
  the wildcard" assertion, an "all asks precede all denies" assertion, and β€” the
  real fix β€” a faithful port of opencode's resolver that pins the EFFECTIVE
  resolved action for a matrix of dangerous commands on EVERY agent. Both
  mutations now go red, each with its own assertion.

ALSO

* Dropped the blanket `read: allow`, which was appended after opencode's
  built-in .env guard and defeated it on every agent.
* Restored `plan`'s built-in `task: {general: deny}` (the global `task: allow`
  had flipped it, giving plan a shell by delegation) and added `write: deny`.
* Closed ~30 unlisted command families: secret reads, kubectl exec/cp/edit/
  replace/rollback, flux delete, privilege wrappers, recursive chmod/chown,
  device writes, systemctl mutations, and the `.`/`$HOME` delete targets.
* `review` could not run `git -C <path> diff` β€” DENIED. In a worktree-first
  workflow that is the only spelling it uses, so it silently fell back to
  read/grep and reported a review it had not performed. Allow-listed the
  `git -C * <verb>*` forms, anchored on purpose.
* browser-agent's isolated config dir existed but was EMPTY, i.e. exactly the
  cold-start it exists to avoid. Added a warm/bootstrap step with an atomic
  lock for concurrent runs, a binary-identity stamp for staleness, and a loud
  degrade-to-global-config on failure. Verified by OUTCOME: bare `opencode
  debug` commands exit 0 while installing nothing β€” only resolving a project's
  .opencode/tools/ triggers the install. Cold 7.0s, warm 0.7s, 4 parallel cold
  runs converge, no lock leak.
* env.js was a checked-in file hardcoding /home/zach with no existence guard,
  duplicating the zsh handle block. Both are now generated from one source,
  nix/agent-handles.nix, and KC_PROD is reconciled into zsh.
* Test-file fixes: handle paths pinned to full literals against a synthetic
  home (the old assertion passed with HOMELAB set to /tmp/totally-wrong and
  with the two kubeconfig targets swapped), and PyYAML is required rather than
  importorskip (a run without it reported 77 passed / 5 skipped / exit 0 while
  silently dropping every agent-permission test).
* browser-agent tests now pin their own OPENCODE_CONFIG_DIR β€” otherwise the new
  bootstrap would judge the operator's real warmed cache stale and delete it.

CORRECTION carried into the docs: the repeated claim that opencode's bash tool
"does not source zsh startup files" is FALSE on this host (v1.18.4). The tool
shell IS zsh and does source .zshenv. The original negative control was real but
misattributed β€” the kubeconfigs are gitignored and absent, so zsh correctly
declines to export KC_HOMELAB while the old UNGUARDED env.js exported it
regardless. The plugin looked load-bearing because it was pointing a handle at a
file that does not exist. It is kept as belt-and-braces for a non-zsh $SHELL.

Verification: 384 tests in the opencode suite (was 82), 162 of which are red at
e353841 and green here. 10/10 mutations red, control green. Real-engine probes
via `opencode debug agent --tool bash` confirm all 18 deny cases across build/
k8s/review. Full repo gate green except two pre-existing dependency failures
(psycopg2, minio) that are identical at e353841.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 2, 2026
…est can't fail (#283)

Two lessons from the 1.4.0 element-ref enrichment leak (homelab-infra fcaca875 / PR #276),
plus three corrections to a developer note that had been wrong since it was written.

reference/element-references.md
- Correct the note: the walk uses previousSibling/nextSibling (NODE variants β€” text nodes
  count), not the Element variants, and slices at 40 chars, not 120 (refText then caps at
  MAX_REF_TEXT_CHARS=80). Both claims were false as written.
- Record the choke point: every page-text read must go through adjacentText β†’ pageText.
  Reading .textContent directly is what shipped the leak. Never add a second "is this a
  text field" predicate β€” that root cause has now recurred seven times.
- πŸ”΄ New section: most obvious tests of this guard CANNOT fail. A native <input>/<textarea>
  never exposes its current value via textContent (typing updates .value), so a canary typed
  into a form field passes under the leaking build too. Three consecutive live captures were
  wasted this way (tasks #147-#149), each read as a pass. Only contenteditable / role=textbox
  and a textarea's SERVER-RENDERED initial content carry user text. Documents the fixture that
  actually settled it (#150), including the positive control that catches a blanket refusal.

SKILL.md
- The extension does NOT ship via Flux: merging to trunk deploys nothing. Brave loads a flat
  copy at ~/clawgate-extension ON THE WORKBENCH, delivered by sync-clawgate-extension.sh, and
  does not hot-reload unpacked extensions.
- A missing .synced-from stamp means a hand-copy: that is how a build which never passed CI
  ran live for ~11 hours while trunk looked clean.
- Workbench firewall allowedTCPPorts is a short allowlist β€” serve scratch test pages on
  localhost, don't open a port for a throwaway.
ZacxDev and others added 2 commits August 2, 2026 15:49
…tic parser

Two rounds of patching permission globs each closed the spellings we thought of
and left the ones we did not. Measured at c1e4c02 by replaying that ref's config
through the resolver model - all resolving ALLOW on the primary agent:

    talosctl -n 192.168.50.94 reset          <- a node wipe, no prompt
    talosctl --nodes=192.168.50.94 reset
    rm -f -r /        rm --recursive --force /
    mke2fs /dev/sdc   mkswap /dev/sdd

because the ask pattern required the tool and the verb to be ADJACENT, and the
rm/mkfs patterns knew one flag order and one binary name. The deny block had been
given infix wildcards; the ask block had not. Separately, the review agent's
allow-list let a stash whose message contained the word "diff" EXECUTE (verified
- it created a stash; the same command without that word was denied), because
opencode's wildcard is an unrestricted dotAll that crosses spaces, so argument
text satisfied the pattern.

A glob over full command text cannot express "this command wipes a node". So the
hard denies move to something that parses.

WHAT THIS ADDS

  scripts/claude-hooks/guard_core.py - a shared, caller-agnostic core with named
  policy sets. It splits a command line on ;/&&/||/|/&, strips VAR= prefixes and
  sudo/doas/env/timeout/nice/... wrappers, recurses into `bash -c` and `eval`,
  and reasons about argv rather than adjacency.

  scripts/opencode/plugin/guard.js - an opencode plugin running that core from
  `tool.execute.before`, throwing on a deny (which hard-blocks the call).

WHICH HOOK, AND WHETHER ask IS EXPRESSIBLE (measured, 1.18.4, 2026-08-02)

  `permission.ask` is in the Hooks type and its output.status is typed
  "ask"|"deny"|"allow", so an ask decision LOOKS expressible. It is not: the hook
  never fired in any probe - not on the allow path, and not on the ask path
  either (an ask rule under `opencode run` printed "auto-rejecting" without the
  hook logging a line). `tool.execute.before` fired on every bash call and
  throwing from it hard-blocks. So DENY is expressible from a plugin and ASK is
  not; ask-grade families stay as globs.

  Also measured: `opencode run` AUTO-REJECTS an ask; only the interactive TUI
  prompts, and `opencode debug agent --tool` auto-APPROVES. ask is friction for a
  human, never a control on an unattended agent.

CLAUDE CODE IS UNCHANGED

  bash-guard.py becomes a thin adapter running the "claude-code" policy, which is
  FROZEN at the original six checks. Proof: the existing suite is 113/113 green
  unchanged, and a before/after decision matrix over a 2,097-command corpus
  differs on 0 rows (positive control OLD-vs-OLD = 0 diffs; negative control
  OLD-vs-one-check-removed = 324 diffs, so the harness can see a change).

  ZERO new denies reach Claude Code. The opencode-only checks are: talosctl
  reset, mkfs/mke2fs/mkswap, dd to a block device, rm -r of / | $HOME | ~ | cwd |
  a top-level system dir, git stash (list/show still allowed), git clean -f, and
  git reset --hard through a -C hop.

  That last one is a GAP IN CLAUDE CODE TODAY, found while doing this: the frozen
  check_git_reset_hard is a raw-text regex anchored on "git reset", so
  `git -C <path> reset --hard` - the worktree-first spelling RULES.md mandates -
  does not match it. Closing it for Claude Code is a one-line policy change and
  is the operator's call, so it is NOT made here.

ALSO IN THIS COMMIT

  * review.md re-states the writing verbs as denies AFTER its allow-list, so
    argument text can no longer ride the `git -C * diff*` pattern.
  * browser-agent: the `command -v opencode` preflight is hoisted above the
    config-dir bootstrap. Without opencode on PATH, readlink -f resolved a bare
    string against cwd, the stamp could never match, and the bootstrap rm -rf'd
    the shared cache on every run - reachable from systemd/cron/a switch. Also:
    the lock wait now derives from OC_WARM_TIMEOUT (it was 30s around a 90s job,
    so the loser force-stole the lock and deleted node_modules while the first
    process was still writing), and a timeout-killed warm (exit 124) is no longer
    stamped valid.
  * test_opencode_config.py: pytest.importorskip("yaml") -> a hard import. The
    comment at :90-92 claimed that had already been done; it had not. Without
    PyYAML the run reported "486 passed, 1 skipped", exit 0, with 384 assertions
    silently gone.
  * The browser-agent tests now assert against COMMENT-STRIPPED source: one
    assertion was being satisfied by a comment saying the code deliberately does
    the opposite.

TESTS

  Red/green: the new tests were run against a pre-change seed tree (c1e4c02
  config + review.md + browser-agent, guard degraded to the six) - 175 failing
  there, 1,171 passing at HEAD. That run also caught a vacuous assertion in one
  of the NEW tests (a bare `command -v` substring that also matched the copy
  inside oc_warm_version, so it passed against the very ordering bug it was
  written for); it is now anchored on the `|| die` preflight.

  Every new rule is exercised across an outer product of nine prefixes, each git
  global-option hop, and each of the five separators. That matrix immediately
  found a real bug in the first draft: `sudo -n <cmd>` peeled wrong, because -n
  had been put in a shared value-flag set for `nice -n 5`, so the node-wipe guard
  silently stopped firing on the most ordinary sudo spelling there is.

  Live end-to-end under a real `opencode run` against a sandbox config dir:
  `mkswap /dev/zzz-nonexistent` and a flag-interleaved talosctl reset were both
  BLOCKED with the guard's message (the sandbox config had bash "*": "allow", so
  only the guard could have stopped them); echo ran as the negative control; and
  with DEVRC_GUARD_CORE pointed at a missing file every call was refused,
  confirming fail-closed.

  Full hermetic suite: 4,015 passed, 1 skipped, 0 errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A 52-mutant sweep built DIFFERENTLY from the suite it attacks (pattern-NARROWING
mutants that keep the function, the policy entry and the deny action, and change
only what matches) killed 39/50 and left 11 survivors. Its own harness was broken
on the first run - a helper argument bug made all 52 "mutants" byte-identical to
pristine, and the pre-flight check passed because nothing was applied. Only the
known-bad control caught it; without that negative control it would have reported
~50 bogus survivors.

Every one of the 11 survivors was a hole in the TEST SUITE, not a defect in
guard_core.py - the code already handled each case and nothing pinned it. That is
exactly the failure mode of the previous round, whose 10/10 sweep had blind spots
only a differently-built sweep could see.

  N16/P14  argv[0] and wrapper tokens are basename-normalised, but no test used
           an absolute path. On NixOS `/run/current-system/sw/bin/talosctl` and
           `/run/wrappers/bin/sudo` are the NORMAL spellings.
  P04      `_peel_variants`' arity branching - the thing the docstring calls the
           reason a wrong flag table "cannot go silently open" - was pure prose.
           Now a generated matrix over every (wrapper, value-flag) pair.
  P13      bundled `-c` shells (`bash -xc '...'`); only bare `-c` was pinned.
  S08      the unlexable-input fallback in `_tokenise`, untested for EVERY check.
  S09      `cd <path> ; git ...`; only the `&&` spelling was pinned.
  S11      private-key headers other than `RSA PRIVATE KEY` - i.e. the wildcard
           that covers modern OPENSSH keys.
  N14      no test pinned any INDIVIDUAL secret shape; deleting three did nothing.
  N15      only the `gh ... create` sinks; `gh pr comment/edit/review` unpinned.
  S12      the public-IP arm - the check whose docstring says it exists because a
           real session leaked an ingress origin IP into a public-repo comment -
           was entirely unpinned, exemptions included.
  S03      the `--` boundary in `_flags_and_operands`. The sweep brute-forced
           1,102 shapes and found it currently safety-NEUTRAL, so it is pinned on
           the helper directly rather than through an outcome that happens not to
           depend on it.

Also strengthened three weak kills the sweep flagged: N13 (`check_git_reset_hard_
argv` was killed by exactly one test - a one-test-deep guard on its own headline
case) and S01/S04 (killed only by over-block assertions, so a relaxation there
would have silently un-covered them).

ONE REAL CODE FIX

The new (wrapper, value-flag) matrix immediately went red on
`timeout -k talosctl reset` and `chrt -p talosctl reset`: for a wrapper that takes
POSITIONALS, the alternate arity branch still hit the positional consumption and
swallowed the real command, so both peelings produced `['reset']`.
`_peel_variants` now branches on positional consumption too. Same class of bug as
the `sudo -n` one the previous matrix caught, and invisible for the same reason -
nothing exercised the branch.

Claude Code re-verified after the parser change: the before/after decision matrix
over the 2,097-command corpus still differs on 0 rows, and test_bash_guard.py is
still 113/113. Full hermetic suite: 4,230 passed, 1 skipped, 0 errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZacxDev
ZacxDev merged commit e21985a into main Aug 2, 2026
@ZacxDev
ZacxDev deleted the feat/opencode-nix-config branch August 2, 2026 21:43
ZacxDev added a commit that referenced this pull request Aug 2, 2026
…tests were not running (#289)

#276 added a FILE to the runner's target list:

    scripts/claude-hooks/tests/test_guard_core.py

and `run_pytest()` guarded with `if [ ! -d "$d" ]`, which rejects a regular
file. The gate reported

    FAIL  scripts/claude-hooks/tests/test_guard_core.py (missing directory)

so the pytest check has been RED on main ever since, and the 913 tests in
that file never ran. The wording made it worse: "missing directory" reads
as an environment fault, so the natural conclusion was "the new #284 gate
is noisy" rather than "a real target is being dropped".

THE FIX
  * `run_pytest()` checks existence with `-e` and branches on `-d`/`-f`
    only to pick the message β€” `python -m pytest` accepts either. Per-target
    parse and attribution are untouched, so #284's guarantee that a
    per-suite collapse stays attributable is preserved.
  * `HERMETIC_DIRS` -> `HERMETIC_TARGETS` (`DEVHOST_DIRS`/`DIRS` likewise):
    the name asserted something false about its own contents.
  * Header comment now says entries may be files, and documents GUARD 5.

NEW GUARD 5 β€” validate the WHOLE list up front, naming every bad entry
  A typo, a moved suite, an unexpanded glob and a file all failed the same
  indistinguishable way, and only once the runner reached that suite. GUARD 5
  checks every entry before anything runs and reports each one with a reason.
  `--check-targets` runs just that guard (no pytest, no tool precondition),
  which is what makes it testable in milliseconds.

  Corrected a claim while writing it: bash DOES expand globs inside an array
  literal (measured β€” injecting `scripts/tests/test_*.py` took the list from
  15 to 32 entries), so only an UNMATCHED glob survives as a literal. The
  comment and the test case now say that instead of the opposite.

MEASUREMENTS (same tree, only the runner differs)
  before  collected=3385   FAIL … test_guard_core.py (missing directory)
  after   collected=4306   PASS … test_guard_core.py (collected=913 passed=913)
  delta   +921 = 913 recovered + 8 from the new test file

  Accepting the target without executing it would look identical from
  outside the gate, so the count is the proof β€” not the exit status.

TESTS (scripts/tests/test_run_tests_targets.py)
  red at origin/main's run-tests.sh: 8 failed in 0.06s
  green at HEAD:                     8 passed in 0.06s

  Reachability, on the REAL list: injecting a bogus entry makes GUARD 5
  exit 2 naming it, and exactly the two list-acceptance tests go red while
  the other six still pass β€” attributable, not a blanket collapse.

  The file labels which of its tests are regression coverage and which is
  an invariant guard, per claude/RULES.md.

NOT fixed here, filed separately: #276 also moved SECRET_PATTERNS out of
bash-guard.py into guard_core.py, so session_insight's
test_patterns_cover_bash_guard parses [] and FAILS on any host where the
hook is deployed. It SKIPS in the nix sandbox, so the flake gate never
sees it. Different file, different failure mode β€” not bundled.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 2, 2026
…s-vs-instances instances

Both measured while unbreaking the pytest gate (#289).

TWO TIERS. 'Gate on the merged tree' extended to environments. The same
suite runs in the nix sandbox and on a dev host, and each environment
silently decides which tests execute β€” so a defect can be permanently
unobservable in the tier you happen to read. #276 shipped THREE
regressions that masked each other:

  (1) a FILE in run-tests.sh's target list rejected by `[ ! -d ]`
      -> gate red, 913 tests never ran
  (2) SECRET_PATTERNS moved to guard_core.py -> the drift test parses []
      and FAILS on a host with the hook deployed, SKIPS in the sandbox
  (3) 10 nix-instantiate tests pytest.fail() without the binary
      -> FAIL in the sandbox, pass on every dev host

(2) and (3) are exact complements; both hid behind (1)'s red.

DECLARATIONS VS INSTANCES gets its third instance in one session:
2 skipif decorators -> 123 tests; 1 list entry -> 913 tests;
1 nix_eval() helper -> 10 parametrized tests. Three is a pattern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 2, 2026
…un in the sandbox (#290)

scripts/tests/test_opencode_config.py's `nix_eval()` shells out to
`nix-instantiate --eval` to pin the GENERATED handle values from
nix/agent-handles.nix, and calls pytest.fail() β€” NOT skip β€” when the
binary is absent, deliberately: 'a skip here is how a wrong kubeconfig
path ships'. `nix` was not in the check's nativeBuildInputs, so all 10
test_handles_resolve_to_the_exact_expected_paths[...] cases failed in the
nix sandbox and passed on every dev host.

This was INVISIBLE until #289: it hid behind the gate being red for an
unrelated reason. Its sibling defect is the exact complement β€”
session_insight's test_patterns_cover_bash_guard fails only where
~/.claude/hooks/bash-guard.py is DEPLOYED and skips in the sandbox. Both
came from #276.

MEASURED, not predicted (the pure-eval argument was a prediction; this is
the check's own output):

  scripts/tests before   collected=959 passed=949 failed=10
  scripts/tests after    collected=959 passed=959 failed=0
  TOTAL                  collected=4353 passed=4351 skipped=2 failed=0
  skips                  pinned 2, observed 2
  nodetests              files=14 tests=468 pass=468 fail=0
  nix flake check        all checks passed

Identical collected count with failures to zero β€” the 10 PASS rather than
merely stopping to fail. The 10-failure diagnosis was confirmed first by a
positive control on a dev host: stripping PATH to python alone reproduced
exactly '10 failed, 455 passed' in that file.

COST, stated as deliberately as the nodejs one above it: +30 store paths,
22 of them the nix closure (nix-{util,store,expr,main,flake,fetchers,cmd}
incl. -dev outputs, plus boost and libarchive), and a nixpkgs bump moving
`nix` now invalidates this check's cache β€” the same trade already
accepted for nodejs. Bought: 10 tests that structurally cannot run
without it, pinning that the handles every agent shell exports resolve to
exact paths. Rejected DEVHOST_TARGETS as the alternative: it keeps the
closure small but weakens the pin to 'runs only where someone remembers
to run it', which is the failure mode this whole area keeps hitting.

Also adds nix-instantiate to REQUIRED_TOOLS so a missing binary is ONE
named precondition failure instead of 10 unexplained assertion failures
deep in the run β€” that guard's entire purpose.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 2, 2026
…be tier-invisible (#291)

test_patterns_cover_bash_guard compared scrub.py's SECRET_PATTERNS against
the DEPLOYED hook (Path.home()/'.claude'/'hooks'/'bash-guard.py') and
skipped when that file was absent. Two problems, and the second is why it
sat broken:

  1. WRONG REFERENT. The deployed copy is generated from the repo file by
     home-manager, so the test answered a question about the machine, not
     about the commit.
  2. IT COULD NOT FAIL IN CI. Keyed on $HOME it SKIPPED in the nix sandbox
     and RAN only on a switched dev host. When #276 moved SECRET_PATTERNS
     out of bash-guard.py into guard_core.py, the ast parser returned []
     and the test failed on every dev host while the hermetic gate β€” the
     tier that gates merges β€” stayed green and silent.

Now compares two files both TRACKED IN THIS REPO
(session_insight/scrub.py vs claude-hooks/guard_core.py), so it runs in
every tier and NEVER skips. Renamed to test_patterns_cover_guard_core.

The empty-parse case is now an explicit, loud assertion naming the cause
rather than the silence that hid #276.

πŸ”΄ run-tests.sh: the conditional EXPECTED_SKIPS entry that pinned the old
environment-dependent skip is REMOVED β€” mandatory, not cosmetic. The test
no longer skips, so leaving the pin would fail the gate with 'FEWER than
pinned'. Replaced with a comment saying why it must not come back.

REACHABILITY β€” both assertions broken on purpose, each failing for its OWN
reason (not a neighbour's):
  * inject a pattern into guard_core.py that scrub.py lacks
    -> 'scrub.py drifted ... no longer covered:
        ['\\bmutant-XYZ-[0-9]{9}\\b']'
  * rename the literal, reproducing #276's move
    -> 'could not parse SECRET_PATTERNS from guard_core.py ...'
guard_core.py restored byte-identical after each.

session_insight suite: 57 passed, 0 skipped (was 56 passed + 1 skipped in
the sandbox / 56 passed + 1 FAILED on a dev host).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 2, 2026
…ations-vs-instances + output-format class, CLAUDE.md CI/byte facts, #273 tab discard, clawgate check-chart) (#287)

* docs: record what 2026-08-02 measured (RULES, CLAUDE.md, tab discard, clawgate)

RULES.md β€” three additions, all from measured ground cases:

* Verify the CONSUMER is running your artifact, not that the deploy
  reported success. `ship.sh` reported "VERIFIED β€” on branch main at
  origin/main + switched" while the browser-bridge unit was crash-looping
  on `OSError: [Errno 98] Address already in use`; an orphaned process
  from the previous day (Aug 1 16:18, in NO systemd cgroup) held
  127.0.0.1:8788 and served the OLD server.py. The converge check
  verified branch + switch and structurally could not see that the
  service never started. Recipe (`ss -lptn` -> `/proc/<pid>/cgroup` ->
  `systemctl show -p MainPID`) was executed live before being written.

* A count of DECLARATIONS is not a count of INSTANCES. A grep of
  `skipif` decorators found "2 node-related skips"; the two decorators
  gated 123 tests (initiatives: 660 passed/123 skipped sandboxed vs
  783 passed/0 skipped with node). A 60x sizing error.

* Sharpen the harness bullet from a MANIFESTATION to a CLASS: when you
  parse a tool's OUTPUT, its format is an unpinned dependency, and "no
  matches" means "possibly the wrong pattern". The file already named
  `diff`'s unified default; that rule was READ this session and the trap
  was hit anyway in three new shapes (a false CLEAN over a 1,445-byte
  difference that only `cmp` caught; node 24's reporter change emptying
  a `^# (tests|pass|fail)` grep; `rc=$?` reading `echo`'s status).

CLAUDE.md β€” correct stale facts in the browser-bridge bullet:

* Drop the hand-pinned "281 B free today". test_skill_size.py owns
  MAX_BYTES/MIN_HEADROOM_BYTES (the floor was raised to 250 in #275);
  point at it instead of re-pinning a figure that rots.
* Record that CI now gates BOTH suites β€” nodetests (#280) and the
  pytest gate's silent-coverage-collapse guards (#284) β€” and the
  headline outcome, skips 125 -> 2.
* Extension 0.7.1; `nav`/`open` accept `--wake[=MS]`.

reference/tabs-instances.md β€” record the #273 measurement: a forced
discard assigns a NEW tabId and releases ownership (484065264 ->
484065273, ownedTabId None), so the stale-documentEmulation hazard is
not reachable on this Chromium. Scope stated honestly (one build, one
profile, one mechanism; auto-discard not exercised; onReplaced inferred,
not observed) and the load-bearing caveat kept: the safety is a property
of the BROWSER, not of the bridge. Placed in reference/, not the
byte-capped core β€” SKILL.md is unchanged at 11,845 B.

clawgate/SKILL.md β€” live version 0.7.82 (embedded kubeclaw chart 0.7.1,
so the pending re-sync note is resolved); "derive from the LIVE pin"
stays primary. Adds the `make check-chart` hazard: it depends on
`sync-chart`, which rsyncs from ~/workspace/kubeclaw β€” that clone sat at
0.3.14 against a vendored 0.7.1, so running it would have clobbered the
deployed chart and reported a false failure. Fetch + `merge --ff-only`
that clone first.

Docs-only. No source file touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(rules): two-tier suites must be green in BOTH; three declarations-vs-instances instances

Both measured while unbreaking the pytest gate (#289).

TWO TIERS. 'Gate on the merged tree' extended to environments. The same
suite runs in the nix sandbox and on a dev host, and each environment
silently decides which tests execute β€” so a defect can be permanently
unobservable in the tier you happen to read. #276 shipped THREE
regressions that masked each other:

  (1) a FILE in run-tests.sh's target list rejected by `[ ! -d ]`
      -> gate red, 913 tests never ran
  (2) SECRET_PATTERNS moved to guard_core.py -> the drift test parses []
      and FAILS on a host with the hook deployed, SKIPS in the sandbox
  (3) 10 nix-instantiate tests pytest.fail() without the binary
      -> FAIL in the sandbox, pass on every dev host

(2) and (3) are exact complements; both hid behind (1)'s red.

DECLARATIONS VS INSTANCES gets its third instance in one session:
2 skipif decorators -> 123 tests; 1 list entry -> 913 tests;
1 nix_eval() helper -> 10 parametrized tests. Three is a pattern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 2, 2026
…nal green state (#293)

The handoff was written mid-session; its 'State now' is superseded. Records the
four regressions from PR #276 that concealed each other (two are exact
complements - one invisible in the sandbox, one on dev hosts), the three
declarations-vs-instances cases, and the preserved operator file on the
workbench that ship.sh was blocked on.

Final: main c9cb47a, both hosts converged, nix flake check green
(pytest 4373/4372/1 skip/0 fail, node 468/468), zero open PRs.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 3, 2026
#297)

RULES.md's "Tool Optimization" section says "Grep over bash grep, Glob over find".
Measured over a 30-day telemetry window that prose rule is not working: Bash is 71%
of all Claude tool calls (workbench 31,355 / laptop 6,164) against 50 Grep+Glob calls
total β€” and ZERO on the laptop. Per RULES.md "Deterministic Over Prose", the
replacement has to be structural, so nudge at the moment the search runs.

search-tool-nudge.py fires PostToolUse on Bash calls that are a tree search
(`grep -r`, bare `rg`/`ag`/`ack`, `find <path> -name`, `ls -R`, `find | xargs grep`,
`find -exec grep`) and injects additionalContext pointing at Grep/Glob. It is a
NUDGE: it never denies and always exits 0, matching shell-env-nudge/audit-pr-nudge.
Deduped to once per KIND (content / files) per session, so a session sees at most two.

Conservative by construction, because false positives on a per-Bash-call hook train
the operator to ignore it. Silent on: a non-recursive grep (single file OR a pipeline
filter such as `git log | grep push`), `rg` fed by a pipe, `find -exec rm/chmod`,
`find -delete`, `find` with no name/path predicate, plain `ls`, search-shaped text
inside a quoted string, and heredoc bodies (`ssh host 'bash -s' <<'EOF' … EOF` runs
on a REMOTE host that Grep/Glob cannot reach).

Also fixes the silent-skip in run-tests.sh's HOOK_TESTS loop: a missing entry was
`|| continue`, the exact #276 shape GUARD 5 exists to prevent. It now fails loudly.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 3, 2026
…run (#306)

`scripts/dl-router/tests` was absent from `scripts/run-tests.sh`'s target
list since the suite was written: 989 tests, zero of them gated. Same
declarations-vs-instances shape as #298 (166 opencode tests) and #276 (913
guard-core tests) β€” one missing line in a target list.

Adding it surfaced two SANDBOX-ONLY portability defects, both of the kind
that are structurally invisible on a dev host:

  * test_setup_script.py's write_pgrep() wrote a stub with
    `#!/usr/bin/env bash`. The nix build sandbox has no /usr/bin/env;
    every NixOS dev host does. Now goes through the helper #298 landed.
  * test_cli.py / test_server_wiring.py hard-coded port 8799 for their
    "sidecar is DOWN/unreachable" assertions. That is a claim about the
    whole machine, not the test β€” an orphaned python3 (pid 2994086, started
    Aug 1 13:40, ppid 1) was listening on it and the test reached a REAL
    dl-router, failing with `sidecar HTTP 409: not_owned_tab`. Ports now
    come from a `closed_port` fixture.

Shared instead of re-derived (RULES.md "One rule, one place"):
  * `scripts/collector/opencode/tests/_mockbin.py` -> `scripts/testlib/
    mockbin.py`, importable by any suite.
  * #298's runtime-shebang scan was scoped to ONE directory, which is
    exactly why it could not see the dl-router defect. The scanner moved to
    `scripts/testlib/shebang_scan.py` and the guard is now REPO-WIDE
    (`scripts/tests/test_runtime_shebangs.py`) with a pinned allowlist that
    fails BOTH ways β€” an unpinned offender and a pin that matches nothing.

MIN_TESTS 2850 -> 5600. The old floor had drifted below HALF the real
total, so a whole 989-test suite could have vanished underneath it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 3, 2026
…ver run (#309)

`scripts/run-node-tests.sh` hard-coded its collection to one directory:

    FILES=(scripts/browser-bridge/tests/*.test.mjs)

so every other `.test.mjs` suite in the repo was invisible to the only check
that runs node. Measured at origin/main (13bc8bd):

  * scripts/dl-router/tests              508 tests, never gated
  * scripts/collector/browser-ext/tests   21 tests, never gated

529 tests, ungated since each suite was written. The gate reported
`RESULT: PASS` with a 468-test total the whole time β€” a hard-coded list cannot
know what it is not looking at. Fourth instance of the same shape (#276: 913
pytest tests behind one list entry, #298: 166, #306: 989).

Adding two lines would leave the NEXT suite ungated identically, so collection
is now DISCOVERY (bash globstar over scripts/**/*.test.mjs) plus a TWO-WAY PIN:

  * a discovered directory absent from SUITES  -> FATAL (forces an accounting
    entry with a measured floor, rather than being swept in under the total)
  * a pinned suite discovery does not find     -> FATAL (the suite vanished)

Discovery alone would reintroduce the silent-collapse hole the old hard-coded
glob at least did not have: an emptied dl-router/tests would just collect fewer
files and still pass over a global floor.

Each suite also runs in its OWN `node --test` invocation with its own TAP
summary and its own floor. A single global floor of 970 is fully satisfied by
browser-bridge (468) + dl-router (508) with browser-ext's 21 tests entirely
gone; per-suite floors make that loud.

A PORTABILITY DEFECT found while building this, worth recording because the
first draft shipped it and the harness hid it: discovery used
`find -printf '%h\n' 2>/dev/null`. `-printf` is a GNU extension, and THREE
different `find`s are reachable from this repo β€” busybox under bash
(~/.nix-profile/bin/find, rejects it), bfs 4.1.1 under the interactive zsh, GNU
findutils in the nix sandbox. Paired with `2>/dev/null` the rejection became an
EMPTY discovery list with no error β€” a false "no suites found". Collection is
now a bash builtin (globstar), which depends on no external binary and behaves
identically in every tier; `test_runner_does_not_use_find_printf` pins it.

MEASURED, both tiers:

  nix build .#checks.x86_64-linux.nodetests   997 tests / 997 pass / 0 fail
  bash scripts/run-node-tests.sh (dev host)   997 tests / 997 pass / 0 fail

  468 (baseline, browser-bridge) + 508 + 21 = 997. No unexplained delta.

scripts/tests/test_run_node_tests_suites.py guards the pin, with its regression
and invariant guards labelled honestly in the module docstring, mutation proofs
that the guard goes red naming the offending directory, and a positive control
on both parsers (the reassuring answer here is an empty set, which an unwired
harness also produces).

The 529 newly-gated tests are NOT regression coverage for this change β€” they
are pre-existing tests whose value is that they now run at all.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ZacxDev added a commit that referenced this pull request Aug 3, 2026
…dition (#310)

* fix(gate): run-tests.sh could not check its own most important precondition

Two measured holes, plus an honest correction to a third.

1. REQUIRED_TOOLS could not express the precondition that matters most.
   It is a list of BINARIES checked with `command -v`. pytest is not a binary
   this runner calls β€” it is a MODULE (`python -m pytest`) β€” so the one guard
   whose entire job is "the thing that runs the tests is present" was
   structurally unable to check pytest. It also asserted `python3` while the
   runner actually invokes `python`.

   MEASURED on the dev host with every REQUIRED_TOOLS binary present but no
   pytest importable: all 17 targets printed

       run-tests: ERROR β€” could not parse pytest's summary for <dir>.

   and the run ended `TOTAL collected=0 … RESULT: FAIL`, exit 1.

   CORRECTION, stated because it changes the severity: the gate did NOT go
   green, and the briefed "reports per-target PASS with collected=0" does not
   exist on this revision β€” GUARD 4 (unparseable summary) and GUARD 3 (the
   collected floor) both fire. The defect is DIAGNOSTIC: seventeen copies of a
   message blaming pytest's OUTPUT FORMAT for a missing dependency, pointing at
   the wrong subsystem. That is the #276 shape β€” a real finding that reads like
   an environment fault. Now one named FATAL, exit 2, before any suite runs.

2. `declare -a RESULTS` / `declare -a SKIP_LINES` leave the arrays DECLARED BUT
   UNSET. Under `set -u` the first `${#arr[@]}` on a still-empty array aborts
   the command with "unbound variable" (measured, bash 5.3.15). With zero skips
   this printed a raw

       scripts/run-tests.sh: line 479: SKIP_LINES: unbound variable

   where GUARD 2's skip list belonged, and the unpinned-skip loop below it never
   executed. No `set -e`, so the script continued and the skip-TOTAL accounting
   still fired β€” the damage was confined to the DIAGNOSTIC path, at exactly the
   moment someone is reading why the gate is red. Fixed with `NAME=()`.

3. "printed RESULT: FAIL and exited 0" did NOT reproduce.
   Measured: exit 1. The structure forbids it β€” `RESULT: FAIL` is printed only
   when `fail != 0` and the next statement is `exit "$fail"`, whose only
   non-zero value is 1. The likely origin is reading the status through a
   pipeline (`| tail`), which yields the last command's status rather than the
   runner's. Pinned end-to-end anyway by
   `test_a_failing_run_never_exits_zero`, which forces a red run and asserts the
   two can never disagree β€” and pytest.fail()s if it could not force one, so it
   cannot pass vacuously.

Known-bad states, each proven LOUD by before/after rather than by reading code:

  no pytest module   before: 17 misleading errors, exit 1
                     after:  one named FATAL, exit 2, no suite started
  empty target dir   fails on the per-directory `collected 0 tests` floor
  typo'd target      GUARD 5 aborts naming the entry and saying "does not exist"

The last two were already guarded before this PR; nothing had ever proven those
paths could fire, so they are labelled REACHABILITY proofs, not regression
coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(test): write the python shim via testlib.mockbin, not a hand-rolled shebang

The repo-wide runtime-shebang scanner (#306, scripts/tests/
test_runtime_shebangs.py) failed this file IN THE SANDBOX ONLY. The dev-host
run was green because I ran this ONE file, and the scanner lives in another β€”
a per-file run structurally cannot see a repo-wide guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant