Skip to content

feat(gemini): install the CLI on linux and fix backend selection - #110

Merged
inkatze merged 17 commits into
mainfrom
worktree-gemini
Aug 10, 2026
Merged

feat(gemini): install the CLI on linux and fix backend selection#110
inkatze merged 17 commits into
mainfrom
worktree-gemini

Conversation

@inkatze

@inkatze inkatze commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

Installs the Gemini CLI on the Linux host, which had the fish snippet exporting GEMINI_API_KEY but nothing that installed the binary or wrote the key, and fixes the reason the work Mac reached for gemini on every review: both /panel-review and /code-review resolved the machine profile from PANEL_REVIEW_PROFILE alone, defaulting to personal, and nothing in this repo ever sets that variable. They now resolve the dotfiles inventory alias, the same indirection scripts/playbook.sh and ollama.fish already use.

How to review: start with CLAUDE.md's new "Review backends: codex vs gemini" section, which is the map. Then roles/claude/tasks/main.yml (the key sync moving out of the Darwin-guarded role) and scripts/claude-gemini-auth-sync.sh (the headless service-account path). The two command files carry the resolver.

  • Files: 16 changed, across 16 commits
  • Reviews run: /polish --nested (4 iterations, converged), /self-review (published this PR), /panel-review --nested (2 iterations, gemini backend)
  • Tests: no unit-test harness exists for this surface; validated by direct reproduction instead — a 10-case resolver table, a 13-case token/permission table across both sync scripts, an 11-case panel-fix table, an end-to-end gemini API call, and the sibling's own harness at 18/18. Details in the collapsed record.
  • Pending sign-off: 10 items, PS-1 through PS-10, each one git revert away.

Four things worth flagging before you read the diff.

The design rationale in the first cut was wrong, and measuring killed it. I split the version pin from its install across two roles to work around mise's npm backend needing node on PATH before roles/environments installs it. That constraint does not exist: with a throwaway MISE_DATA_DIR and node both uninstalled and absent from PATH, mise install npm:<pkg> succeeds, because mise bootstraps its own node. gemini is now one line in linux_mise_tools and the two bespoke tasks are gone.

The security finding did not survive reproduction. A review agent reported that --skip-trust gives drive-by code execution inside a reviewed PR via .gemini/settings.json. I built that exact payload; it was never executed, and the chunk filenames the agent cited do not exist in this install. What survives is a real but lower-severity point: the CLI never needs the untrusted tree as its cwd, since input arrives on stdin. Both commands now run it from a fresh mktemp -d in a subshell.

The review loop repeatedly caught its own fixes. The fast-path chmod added in one pass would have failed a converged run on a root-owned key file; the whitespace guard added in another did not catch whitespace, and the test that "verified" it passed spuriously by asserting only that some FAILED: line appeared. Both are fixed, with the case lists tightened.

And the branch was dogfooded through the thing it changes. /panel-review --nested ran its own gemini backend against this diff, using the mise-installed binary, the new alias resolver, and the new mktemp -d + subshell + --skip-trust invocation. Two findings are worth calling out. Its pre-flight correctly stopped with Gemini CLI not installed; mise run linux will install it — because ~/.config/mise/conf.d/linux.toml symlinks to the main checkout, so the pin is not live until this merges. That is the right behaviour and the corrected remediation string. And it found four defects nine Claude lens agents across four polish iterations had missed, three of them introduced by those iterations' own fixes — including that the gemini bullet spends a paragraph warning that a /tmp prompt file exposes the whole diff of a private repo while the Ollama block five lines below did exactly that. A non-Anthropic model noticed that my own prose indicted the code beneath it. That is the variance the panel exists for.

Worth knowing for next time: a command change cannot be dogfooded through its own slash command from a worktree, because ~/.claude/commands symlinks to the main checkout. The run above followed this branch's instructions manually.

Audit record — 4 polish iterations, converged

Lens coverage

Lens Findings Notes
Correctness, logic, edge cases 6 alt regression, empty-alias-file, undeclared-tool gate, chmod regression, whitespace guard, -e widening
Security 5 All zone-screened and paused for human direction; 1 refuted by reproduction
Error handling and failure modes 3 Unguarded sync aborting the last role; bare cat under set -eu; trap on EXIT only
Performance 1 Redundant check task, dissolved by the redesign rather than optimised
Concurrency / state 2 Tag split meant neither mise run alone gave a working backend; temp-file leak
Naming, readability, structure 1 Token-block duplication across two scripts (declined, follow-up)
Documentation 21 Largest bucket across all iterations: false parity claims, contradictions, 4 stale tables, repeated miscounts
Tests / verification 4 No harness for the sync script; CI reaches none of the new tasks
Cross-file consistency 8 Resolver in 4 places; the two command files repeatedly drifting

Auto-applicable

# Finding Tool + rule Fix Commit
none Every project gate stayed green throughout, so nothing met the tool-grounded predicate

Agent-resolvable

# Finding Test Commit
none Bucket unavailable: no active kickoff brief for this branch, so the brief-alignment predicate cannot be satisfied

Needs sign-off

# Finding Fix applied Route reason Commit ID
1 Node-on-PATH rationale for the two-role split was factually wrong Moved to linux_mise_tools, deleted both tasks Multi-step; changes which role and tag install it 3cd9d25 PS-1
2 Resolver dropped the alt branch, tested [ -f ], ignored DOTFILES_HOST_FILE Restored all three Behaviour change to agent-followed instructions 50977df PS-2
3 Unguarded sync aborts the last role, taking planwright's install with it op --version probe + skip notice, mirroring roles/ssh Changes failure semantics 0bfeebe PS-3
4 Four secret-handling gaps in the key sync Empty/unreadable guards, per-call token scoping, fast-path chmod Secrets zone; applied on direction 9ae844f PS-4
5 Same two gaps in the script the block was copied from Ported the guards Secrets zone; applied on direction 62870b9 PS-5
6 --skip-trust runs with folder trust off inside a working tree Run from a scratch dir, both commands Security control; applied on direction c0f7003 PS-6
7 PS-4's chmod would fail a converged run on a root-owned key file Chmod only when the mode is wrong; widened trap; fixed whitespace guard Regression in a prior fix ed7b19b PS-7
8 PS-6 pointed at /tmp, which is world-writable, and leaked cwd mktemp -d + subshell; six doc corrections Regression in a prior fix 59b90b0 PS-8
9 Ollama prompt still in /tmp; scratch dirs leaked; caller-supplied token skipped the whitespace guard; op_err untrapped; ssh script still exported globally All five Panel-surfaced; secrets zone; applied on direction 9dc0eec PS-9
10 PS-9's scratch trap covered EXIT only and mktemp -d was unchecked || exit 1 + EXIT INT TERM HUP on all three blocks Regression in a prior fix 58a0d8c PS-10

Needs human judgment

# Fork Ladder record Outcome
1 Scope of the four secret-handling fixes rung 3: sibling-script precedent Resolved by human: apply all four, and port to the sibling
2 --skip-trust posture rung 2: research + direct reproduction Resolved by human: neutral cwd in both commands

Declined log

# Finding Rationale
1 /tmp prompt file world-readable (Ollama backends) Pre-existing; partly addressed for gemini by the scratch-dir move
2 Token-block duplication across two scripts Real, but scripts/ has no shared-helper convention; recorded rather than invented
3 ssh-lan-config-sync.sh still exports the token process-wide Outside the scope the human approved; it makes a single op call with no second secret in flight
4 specs/pair-flow D-6 not formally amended Over-citation removed at the call site instead; amending a signed spec bundle is a separate decision
5 Resolver snippets are bash, not fish Pre-existing shape; the Bash tool runs bash
6 playbook.sh accepts an empty alias file (-l "" = no limit) Real latent bug, but editing the playbook entrypoint is outside this change; documented in CLAUDE.md
7 No test harness for claude-gemini-auth-sync.sh; CI reaches none of the new tasks Genuine gaps, but new work rather than defects in this diff

Pending sign-off

  • PS-1 Gemini CLI installs from linux_mise_tools instead of a two-role split · commit 3cd9d25
    • Route reason: multi-step, changes which role and tag perform the install
    • Reject with: git revert 3cd9d25
  • PS-2 Alias resolver restores the alt branch, empty-file guard and DOTFILES_HOST_FILE · commit 50977df
    • Route reason: behaviour change to instructions an agent executes
    • Reject with: git revert 50977df
  • PS-3 Key sync probes for op before running · commit 0bfeebe
    • Route reason: changes failure semantics of a role that runs on every host
    • Reject with: git revert 0bfeebe
  • PS-4 Token and key-file handling hardened in the Gemini sync · commit 9ae844f
    • Route reason: hard-disqualifier zone (secrets); applied on explicit direction
    • Reject with: git revert 9ae844f
  • PS-5 Same guards ported to ssh-lan-config-sync.sh · commit 62870b9
    • Route reason: hard-disqualifier zone (secrets); applied on explicit direction
    • Reject with: git revert 62870b9
  • PS-6 Gemini runs from a scratch directory in both commands · commit c0f7003
    • Route reason: security control change; applied on explicit direction
    • Reject with: git revert c0f7003
  • PS-7 Fast-path chmod no longer fails a converged run; trap widened; whitespace guard fixed · commit ed7b19b
    • Route reason: corrects a regression introduced by PS-4
    • Reject with: git revert ed7b19b
  • PS-8 Scratch dir is mktemp -d in a subshell, plus six doc corrections · commit 59b90b0
    • Route reason: corrects a regression introduced by PS-6
    • Reject with: git revert 59b90b0
  • PS-9 Five panel-surfaced fixes: Ollama prompt out of /tmp, scratch cleanup, caller-token guard, op_err trap, ssh token scoping · commit 9dc0eec
    • Route reason: hard-disqualifier zone (secrets); applied on explicit direction
    • Reject with: git revert 9dc0eec
  • PS-10 mktemp -d guarded and scratch trap widened to INT TERM HUP · commit 58a0d8c
    • Route reason: corrects a regression introduced by PS-9
    • Reject with: git revert 58a0d8c

Pass summary

  • Mode: /polish --nested × 4 iterations (converged), /self-review standalone for publication, then /panel-review --nested × 2 iterations against the gemini backend (dogfood).
  • Panel backend: gemini-cli 0.54.4, 152s and 142s per pass, invoked exactly as this branch documents (fresh mktemp -d, prompt inside it, subshell cd, --skip-trust --approval-mode plan, prompt on stdin). Output was a clean markdown table both times with no reasoning-trace leakage. Panel iteration 1 surfaced 4 findings + 1 re-raise, all applied; iteration 2 surfaced 3, two applied and one declined as pre-existing.
  • Panel pre-flight: stopped correctly on command -v gemini with the corrected Linux remediation string, because the mise pin is not live until merge. The backend was then invoked through its install path directly, the only deviation from the documented invocation.
  • Base: origin/main @ cf22d10, pinned at pre-flight.
  • Kickoff brief: none. worktree-gemini is not a planwright/<spec>/task-<ids> branch and this work is not a declared task in any bundle, so the Agent-resolvable bucket was unavailable for the whole run. Declared rather than stretching linux-migration's brief to cover it.
  • Discovery: parallel lens fan-out, 9 read-only sub-agents on iteration 1, narrowing to 4, 2 and 1 as the finding list drained. Scoping declared at each step.
  • Tooling (green throughout): yamllint, ansible-lint (production profile), ansible-playbook --syntax-check, shellcheck --severity=warning, gitleaks with the repo config, skill-contracts.sh.
  • Wider suite: scripts/ssh-lan-config-sync-test.sh 18/18 after every change to that script.
  • Reproductions run: resolver edge cases before and after (10 cases); mise ls --missing on an undeclared tool; npm backend with node absent from PATH; npm backend with a throwaway MISE_DATA_DIR; declared-tool re-install convergence; the --skip-trust MCP payload; token/permission guards (13 cases across both scripts); end-to-end Gemini API call.
  • Convergence: polish iteration 4 returned none; panel iteration 2's residual was two one-line consistency fixes plus one declined pre-existing item. No safety condition fired. Two hard pauses (zone screen) resolved by human direction.
  • Dogfood caveat: ~/.claude/commands symlinks to the main checkout, so /panel-review invoked as a slash command served main's copy, not this branch's. The run followed this branch's instructions manually. A command change cannot be dogfooded through its own slash command from a worktree.
  • Observations recorded: 3, for /spec-draft.
  • Not verified: the Ansible tasks have not been executed. Running the playbook from this worktree would repoint ~/.claude and ~/.config/mise.toml into a temporary directory, so it must be run from the main checkout after merge.

inkatze added 17 commits August 10, 2026 12:48
The panel/code-review gemini backend was only ever installed from the
Brewfile, so the Darwin-guarded osx role left the linux host without it
while roles/fish still symlinked conf.d/gemini.fish there. That host was
exporting GEMINI_API_KEY for a binary it did not have.

apt has no package and mise's registry offers exactly one backend
(npm:@google/gemini-cli), so npm is the route rather than a preference.
The pin sits in linux.toml with the other linux-only pins, but the
install runs from the environments role: the npm backend needs node on
PATH and roles/linux runs first, so linux_mise_tools is walked before
node exists.

Gated on 'mise ls --missing' rather than on a string in mise's output.
mise emits 'WARN gemini installed but not activated' on every run while
the tool is undeclared, so the linux role's 'installed in stderr' test
would report changed forever.
The ~/.gemini directory and the 1Password sync lived in the
Darwin-guarded osx role, so the linux host got the fish snippet that
reads the key but nothing that writes it. Neither task was ever
macOS-specific; both move to the cross-platform claude role and keep the
osx tag so 'mise run osx' still syncs on a Mac.

Teach the sync script the headless path. A headless host has no
1Password desktop app to authorize against, so op fails before it reads
anything; fall back to the machine-local service-account token exactly
as ssh-lan-config-sync.sh does. That constrains the vault: a service
account cannot be granted Personal or Private, so the item has to live
in Dotfiles Service Account and must be addressed with an explicit
--vault. Without one op refuses every field with 'a vault query must be
provided', which reads like a missing item and is not.

Moving the item between vaults also reassigned its id, so ITEM_UUID now
names the item in that vault rather than the one it had in Private.

Verified end to end on the linux host: CHANGED on first run, OK on the
second, key file 0600 and the directory 0700 (the gemini CLI creates
that directory itself at 0755, so the mode assertion is load-bearing).
/panel-review and /code-review resolved the machine profile from
PANEL_REVIEW_PROFILE alone, defaulting to 'personal'. Nothing in this
repo ever sets that variable, so the work host fell through to the
default and reached for gemini on every review, the exact opposite of
what the profile table asks for.

Resolve the dotfiles inventory alias instead (DOTFILES_HOST, else
~/.config/dotfiles/host, else work), the same indirection
scripts/playbook.sh and roles/fish/files/ollama.fish already use, so the
work host picks codex with nothing to remember. PANEL_REVIEW_PROFILE is
still honoured ahead of it as a per-run override. Note the fallback
direction deliberately differs from ollama.fish: there an unresolved
alias must set nothing, here it means work, matching playbook.sh.

Fold server into the personal/alt row, and record gemini CLI drift
measured on 0.54.4: the CLI now refuses headless runs in untrusted
directories, and it downgrades --approval-mode plan to default BEFORE
aborting, so a future version that stops aborting would run with the
read-only guard already stripped. Both commands now pass --skip-trust
and keep plan mode.
…g-sign-off]

The previous commit split the pin from its install across two roles, on the
premise that mise's npm backend needs node on PATH and that roles/linux runs
before roles/environments owns the node pin. Measurement refutes the premise:
with a throwaway MISE_DATA_DIR (node uninstalled) and node absent from PATH,
'mise install npm:is-odd@3.0.1' still succeeds, because mise bootstraps a node
for the backend rather than borrowing the host's.

So gemini joins linux_mise_tools like every other entry, and the two bespoke
tasks in roles/environments go away. That also closes a real hole those tasks
had: 'mise ls --missing <tool>' prints nothing both when a tool is installed
AND when it is merely undeclared (verified with an uninstalled, undeclared
'deno'), so the gate could not tell convergence from a missing linux.toml
symlink. A tag-filtered 'mise run environments' on a host whose linux role had
not run would have skipped the install silently and reported success — and
'mise run environments' is exactly what the new error strings told the operator
to run.

Confirmed the linux role's existing loop converges for this tool: once gemini
is declared, a re-install prints nothing on stderr, so its
changed_when: "'installed' in stderr" test stays false. The 'installed but not
activated' WARN that would have broken it only fires while the tool is
undeclared.
… resolver [pending-sign-off]

The resolver introduced two commits ago claimed to work 'exactly as
scripts/playbook.sh and ollama.fish' do, and did not. It dropped their residual
'*panela*' hostname match, so an alt Mac (which legitimately has no alias file,
per the machine-local files section of CLAUDE.md) resolved to work and reached
for codex, a CLI it never logs into. Fixing the work host by breaking the alt
host is not a fix.

It also tested '[ -f "$alias_file" ]' rather than the file's contents. An empty
or newline-only alias file yields an empty profile, which is not 'work' and so
falls through to gemini. Reproduced: a touched alias file puts the work host
back on gemini, which is the exact bug the change exists to fix, re-entered
through a different door. The branch now binds to a non-empty trimmed value.

Also honours DOTFILES_HOST_FILE, which playbook.sh honours and this did not, so
a host that relocates its alias file no longer has playbook.sh and the review
commands disagreeing about which machine it is.

Validated with a 10-case table covering all three regressions and seven
already-correct cases: alt-with-no-file, empty file, newline-only file,
unconfigured non-alt host, each named alias, alias-beats-hostname, and both
override precedences. All three failing cases were confirmed failing against
the previous snippet first.

Mirrors the whole block into /code-review, including the two elements it was
missing (the not-in-table default note and the service-account vault caveat),
and makes /panel-review's Maintenance section name the reciprocal sync duty so
drift is catchable from either side rather than only from one.
…-off]

Splitting detection from the sync is what roles/ssh already does for the LAN
alias sync, and its comment records the reason: a host without the 1Password
CLI genuinely cannot do this and should be skipped, while a host that has op
and still fails is a real error. Hard-failing on a merely absent op puts
linux-migration Task 7's 'two consecutive runs, zero failed tasks' convergence
out of reach.

That split was unnecessary while these tasks sat in the Darwin-guarded osx
role, because the Brewfile guaranteed op on every Mac that ran them. Moving
them into the unguarded claude role removed the guarantee without replacing it,
so a non-work host lacking op would abort the LAST role in main.yml partway
through, taking the planwright marketplace and plugin installs below it with
them, on every subsequent run.

Also drops an over-citation: the task comment claimed D-6 in
specs/pair-flow/design.md as the authority for server using gemini. D-6
predates this host and names only work, personal and alt. It is background
here, not authority; the current mapping lives in the repo CLAUDE.md.
Batched documentation corrections from the review pass. Each was a statement
this branch made false, or left false:

- The 'Review backends' section claimed the profile 'is not a per-run choice',
  then documented two per-run overrides in the next paragraph, and silently
  dropped the three --backends-only backends. It also asserted parity with
  playbook.sh and ollama.fish while omitting their alt branch, and asserted it
  twelve lines before noting the fallback deliberately differs from
  ollama.fish. Both claims are now accurate and the one real divergence is
  named once.
- The Linux install rationale documented an ordering constraint that
  measurement showed does not exist; it now records what was measured and why
  the earlier split was abandoned, so the dead end is not re-walked.
- The tag story was half told: the sync carries osx AND linux tags, which is
  the only place a platform tag names a task outside its platform role. Both
  the consequence and the missing 'mise run claude' task are now stated.
- Two machine-local table rows had gone stale: 'host' gained the two review
  commands as readers, 'op-service-account-token' gained the gemini sync.
  README gained the token row it never had, including how each of the two
  consumers degrades without it.
- The vault section spoke of one item and one script; there are now two of
  each. Records the blast radius that creates, and that moving an item between
  vaults reassigns its id.
- gemini.fish still described itself as the personal/alt read side.
- The --skip-trust note asserted --approval-mode plan compensates for the
  bypassed folder-trust gate. A direct test on 0.54.4 (a project
  .gemini/settings.json MCP server, run under --skip-trust --approval-mode
  plan) did not execute it, so the section now reports what was measured
  rather than what was assumed in either direction.
Seed material for /spec-draft, not acted on here: the mise ls --missing gate
being blind to undeclared tools, claude-mcp-sync-github.sh still being
Darwin-only while roles/claude went cross-platform, and roles/claude having
neither a mise task nor a CI matrix entry.
…g [pending-sign-off]

Four findings from the review pass, applied together on your direction because
they share one file and one class. All were latent rather than active: none
fires on this host today.

- An EMPTY token file passed the -f test and the mode check, and the resulting
  empty OP_SERVICE_ACCOUNT_TOKEN switched OFF the desktop-app path that would
  otherwise have worked, so a Mac failed with an error naming the item and the
  vault while the real fault was a placeholder file. Now -s, with a message
  that names the file.
- An unreadable token file (mode 600 but root-owned, which is what creating it
  under sudo leaves) aborted on cat's own status under set -eu, ending the run
  with a raw 'Permission denied' and none of this script's FAILED: convention.
  Both reads now route through fail().
- The token was exported process-wide, so it was inherited by every later
  child including the sh -c that holds the plaintext API key, which carried
  both secrets at once. It is now scoped to the single op call via an op_get
  helper, which is what this file's own header already claimed.
- The fast path returned OK without asserting the key file's mode. chmod 600
  ran only on the slow path, so a file with the right bytes at the wrong mode
  stayed loose forever, since no later run rewrites matching content. Reachable
  on exactly this host: the fish snippet read that path long before anything
  wrote it, and the gemini CLI creates ~/.gemini itself at 0755.

Validated with a four-case table against the real script (empty,
whitespace-only, bad mode, absent) plus an end-to-end regression for the last
one: chmod 644 the key file, run, observe OK and the file back at 0600.
…ending-sign-off]

The token-file block in claude-gemini-auth-sync.sh was copied from this script,
so the empty-file and unreadable-file gaps the previous commit fixed were
inherited from here rather than invented there. Fixing only the copy is the
two-site drift that made the duplication worth flagging in the first place.

Scoped to those two guards. This script's export stays process-wide, because
it makes a single op call with no second secret in flight, so the narrowing
that mattered for the gemini script buys nothing here.

Its own harness still passes 18/18 (scripts/ssh-lan-config-sync-test.sh),
which covers the happy path, idempotent re-run, unresolved reference, symlink
refusal and unparseable render. The four new token cases were checked against
this script too.
…g-sign-off]

Both commands invoke gemini with --skip-trust, which is unavoidable for a
headless run: without it the CLI downgrades --approval-mode plan to default and
then aborts. That leaves the folder-trust gate off inside a working tree, and
/code-review's tree is someone else's checked-out PR.

The prompt already reaches the model on stdin, so neither command ever needed
the tree as its cwd. Running from a scratch directory means --skip-trust has
nothing to trust: no .gemini/settings.json, project hooks, skills or GEMINI.md
can load from the reviewed code at all.

Explicitly defence in depth, not a patched hole. A direct test on gemini-cli
0.54.4 -- a .gemini/settings.json declaring an MCP server whose command writes
a marker file, run under --skip-trust --approval-mode plan -- did NOT execute
it. The earlier claim that this was drive-by code execution did not survive
reproduction, and the note says so rather than implying a fix for a live
vulnerability.

Also drops GEMINI_CLI_TRUST_WORKSPACE=true as a suggested equivalent: the flag
is per-invocation, while the env var trusts every directory for every later
gemini run in that shell.
…n [pending-sign-off]

Second review pass, against the previous two commits' own fixes.

- The unconditional 'chmod 600' added to the fast path turned the steady state
  into a hard failure on a host where the key file is owned by someone else:
  content matches, so the old code printed OK, and an EPERM there fails the
  Ansible task and aborts the last role in main.yml on EVERY run. That is the
  failure the op probe was added to prevent, re-entered through a different
  door. It now chmods only when the mode is actually wrong, and says to check
  the owner when it cannot.
- The directory got the same treatment, which the file-mode fix's own comment
  argued for and then did not do: the gemini CLI creates ~/.gemini at 0755, and
  on a converged host the slow path never runs again to correct it.
- The whitespace guard did not guard. '$(cat)' strips trailing newlines and
  nothing else, so a file holding '   ' yielded a non-empty token that reached
  op and came back as 'failed to parseToken, format is invalid' -- an error
  about the token's shape when the fault is a placeholder file. Both scripts now
  test a whitespace-stripped copy while passing the real token through
  untouched. The first pass's test asserted only that SOME FAILED: line
  appeared, so it passed spuriously; the case list now asserts the message.
- '-e' without a regular-file test captured a directory at the token path,
  failing with a confusing 'is mode 755'.
- op_get was a no-op when a caller had already exported the token, so the
  export path kept the behaviour the helper exists to remove. The exported copy
  is now hoisted into op_token and unset, making op_get the only route.
- The temp-file trap covered EXIT only, so an interrupt between write and
  rename left a plaintext key at ~/.gemini/.api-key.XXXXXX that nothing would
  ever clean up. Now EXIT INT TERM HUP.

Validated with a 13-case table (three whitespace shapes across both scripts,
directory-at-path, converged 600, loose 644, loose 755 dir, trap contents, no
leftover temps) plus the sibling's own harness at 18/18.
…'s own claims [pending-sign-off]

Second review pass, against the previous commits' documentation.

The scratch-directory guidance relocated the trust rather than removing it. It
said to cd to 'the directory holding $prompt_file', and the only prompt-file
convention either command defines puts that in /tmp -- world-writable, so any
local user can pre-plant /tmp/GEMINI.md or /tmp/.gemini/settings.json and have
a --skip-trust run load it, persistently, across every later review. Both
commands now require a fresh 'mktemp -d'. They also now use a subshell: this
session's shell keeps its cwd between tool calls, so a bare cd would have
stranded every later git and gh step of the review outside the repo, including
/code-review's checkout restore. The limits are stated too, since the old
wording implied more than it delivered: user-level ~/.gemini/ config loads
regardless of cwd, and plan mode still lets the model read tree files by
absolute path.

Corrections to claims the last pass introduced or left standing:

- 'Both scripts address their item by id' was false. ssh-lan-config-sync.sh
  uses the item NAME via op:// references, so a vault move does not touch it.
- 'Two consequences' had grown to three bullets.
- 'this one Claude task' undercounted the four tasks carrying the osx+linux
  tags.
- The README token row had both halves of its degradation story backwards: the
  probe skips on op being ABSENT, not on the token being absent, and a Mac with
  an unlocked desktop app needs no token file at all.
- The CLAUDE.md resolver paragraph again claimed parity with playbook.sh while
  being deliberately stricter than it. It now says so, and names the latent bug
  that strictness avoids: playbook.sh tests mere existence, so a touched alias
  file there produces 'ansible-playbook -l ""', which Ansible reads as no limit
  and runs every inventory host against the machine. Left unfixed on purpose;
  this change has no business editing the playbook entrypoint.
- panel-review's Maintenance clause cited /code-review 'step 1b', which is the
  Slack step; the mirror is step b.
- The two files' gemini flag order had drifted apart again.
- The skip notice said the key file was 'left as-is' and then that the variable
  would be unset, without connecting the two.
- The skip notice now carries the same CI gate as the task it reports on, so a
  CI runner stops printing an auth warning about commands it never runs.
Third review pass. The correctness lens came back clean; these are the six
remaining text defects, all introduced by the two preceding passes.

- CLAUDE.md said two resolver clauses were dropped in the first cut; three
  were. Both command files already said three. DOTFILES_HOST_FILE was the
  third.
- The tag-rationale comment in roles/claude/tasks/main.yml still said 'both
  tasks' after the probe and skip notice took the count to four. This is the
  same miscount the previous pass fixed in CLAUDE.md and did not chase into
  the comment that introduces the rule, or into the observation entry, which
  said two as well and called a directory-creation task a 1Password one.
- panel-review left its prompt file in /tmp while telling the operator to cd
  elsewhere, and cited that convention as the reason for mktemp -d. code-review
  had already moved the prompt into the scratch directory, so two files that
  must mirror prescribed different homes for the same artifact. panel-review
  now matches, which also fixes the prompt file's mode: it carries the entire
  diff of a private repo and was created world-readable in /tmp.
- panel-review's reciprocal sync pointer said 'step b'; /code-review refers to
  that block as step 5b in its own text.
- The supersession note on the decouple-review-tooling observation sat inside
  the 'Open decision' section only, so three other passages still asserted
  PANEL_REVIEW_PROFILE as the current profile mechanism. Moved to the header
  where it scopes them all.
…g-sign-off]

Dogfood pass: /panel-review's own gemini backend, run against the diff that
changes /panel-review. It found four things nine Claude lens agents across four
polish iterations did not, three of them introduced by those iterations' fixes.

- The gemini bullet spends a paragraph explaining that a /tmp prompt file
  exposes the whole diff of a private repo, and the Ollama block five lines
  below it kept doing exactly that. Both now use a 0700 mktemp -d, which also
  settles the collision case the $$ namespacing was there to make unlikely.
- The mktemp -d guidance added two commits ago never cleaned up, so every
  review leaked a directory containing the full diff. Both commands now trap.
- The token hoist added two commits ago validated the file path and let a
  caller-supplied OP_SERVICE_ACCOUNT_TOKEN bypass the whitespace guard
  entirely. Both scripts now hold both routes to the same bar.
- op_err was mktemp'd once per loop iteration and removed only on the normal
  paths, so an interrupt during any op call leaked a file with nothing to clean
  it up. It is now created once, before the loop, under a trap.

Plus the sibling-drift item the panel raised independently after it was scoped
out of an earlier round: ssh-lan-config-sync.sh now scopes its bearer token
per-call through op_run, matching op_get. That script spawns sed, grep, cmp,
chmod, mv and an 'ssh -G' after the injection, and an exported token sat in the
environment of every one of them.

Validated: 11-case table (both scripts' caller-token guard, op_err trap and
single mktemp, op_run wiring and absent export, both docs' prompt/scratch
handling, end-to-end sync), the sibling harness at 18/18, and the full gate set
green.
…-off]

Panel iteration 2, against iteration 1's own fixes.

- The scratch trap covered EXIT only, in the same commit that widened
  claude-gemini-auth-sync.sh's trap to INT TERM HUP for exactly this reason.
  Ctrl-C during a multi-minute backend call is the likeliest way one of these
  runs ends early, and it would have left the full diff behind.
- mktemp -d's failure was unchecked, so a failure left $scratch empty and
  everything after it operated on the wrong path.

Both apply to all three scratch blocks across the two commands. The panel's
third finding (sed's pipe delimiter against a $VAULT containing a pipe, in
ssh-lan-config-sync.sh) is declined: pre-existing, untouched by this branch,
and the backend rated its reachability very low itself.
Surfaced by running /panel-review --nested against a branch that changes
/panel-review: ~/.claude/commands symlinks to the main checkout, so the skill
loader served main's copy. Same for the mise conf.d pin. Seed material for
/spec-draft, not acted on here.
@inkatze
inkatze marked this pull request as ready for review August 10, 2026 21:36
@inkatze
inkatze merged commit 6bc7d6b into main Aug 10, 2026
2 checks passed
@inkatze
inkatze deleted the worktree-gemini branch August 10, 2026 21:37
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