Skip to content

fix(secrets,worktrees): fix secret redaction leakage and prune stale worktrees - #855

Open
euxaristia wants to merge 32 commits into
Gitlawb:mainfrom
euxaristia:fix/secrets-redaction-and-worktrees
Open

fix(secrets,worktrees): fix secret redaction leakage and prune stale worktrees#855
euxaristia wants to merge 32 commits into
Gitlawb:mainfrom
euxaristia:fix/secrets-redaction-and-worktrees

Conversation

@euxaristia

@euxaristia euxaristia commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Closes a tail-leak edge case in secret redaction and adds an anthropic_key pattern; extra secret values are sorted by length descending so a shorter match can't shadow a longer one, and worktree paths are canonicalized before comparison
  • Adds recoverable PID-based leases for worktrees: reclaims released or crashed worktrees, proves Prepare ownership with a git-admin marker, and scopes unlock/release/clean to locks actually owned by the caller
  • Fixes real data-loss risks: preserves orphaned commits instead of discarding them, verifies release ownership before touching a worktree, checks removal exit codes and fails closed on inspection errors, and handles os.Chtimes errors and legacy worktree cleanup on reuse
  • Adds -C to worktrees release, completes worktrees in shell completions, and adds regression tests for the aggregation, lease-detection, and nested-activity paths this touches

Test plan

  • go test ./internal/worktrees/... ./internal/secrets/... ./internal/cli/...

Summary by CodeRabbit

  • New Features
    • Added worktrees release to safely unlock and release worktrees, with path and working-directory support.
    • Worktree preparation now manages locks, ownership, stale leases, and cleanup automatically.
  • Bug Fixes
    • Improved protection against lock collisions, stale worktrees, and cleanup failures.
    • Expanded detection and redaction for OpenAI, Anthropic, JWT, GitHub, Google, Slack, AWS, Stripe, and GitLab credentials.
    • Improved redaction of credentials with suffixes, trailing characters, or nested formats.
  • Documentation
    • Updated worktree help and recovery guidance.

euxaristia and others added 26 commits July 22, 2026 12:50
…worktrees

1. Prevent trailing redaction leaks in github_token, aws_access_key_id, and google_api_key by adding trailing word boundaries and allowing variable lengths. Refine the openai_key pattern to cleanly distinguish legacy keys and modern prefixed keys (sk-proj-, sk-svcacct-) from ordinary kebab-case phrases.
2. Implement auto-pruning of zero-owned git worktrees older than 24 hours at the start of worktrees.Prepare to prevent indefinite disk space leaks.
…oss risk

Drop the trailing \b anchor on the four secret patterns whose body
class allows "-" (slack_token, google_api_key, the modern openai_key
branch, jwt). \b requires a word/non-word transition, so a secret
ending in "-" right before a delimiter has none, and the engine
backtracked the greedy quantifier to drop that last character instead
of failing the match, leaking it. The body character class already
provides the real stopping boundary, so the anchor was unnecessary.

Fix two issues in worktree Clean flagged in review:

- Staleness was decided by the worktree directory's own mtime, which
  only changes when an entry is added/removed/renamed directly inside
  it, not when a long-running task edits existing files deeper in the
  tree. Clean now walks the tree and treats any recently modified entry
  as live, and also skips any worktree a caller has explicitly locked
  via git worktree lock.
- baseDir ownership used a raw strings.HasPrefix, so a sibling like
  "<baseDir>-other" would false-match. Replaced with a filepath.Rel
  path-boundary check.
…n errors

defaultRunGit deliberately returns a nil error alongside a nonzero
CommandResult.ExitCode for a failed git invocation, so the worktree
remove call must check ExitCode itself instead of trusting a nil error
to mean success. Route it through gitOutput, which already does that.

worktreeIsStale treated an inspection failure (an unreadable file, a
WalkDir error) the same as "keep walking," which can let an
incompletely-inspected worktree be judged stale. Any inspection error
now makes it ineligible for removal instead.
…to owned worktrees

The scanner's trailing \b anchors made a credential vanish entirely when
followed by a word character outside its body class (an appended suffix
like AKIA...EXTRA, or ghp_..._suffix): the fixed or unbounded-greedy
quantifier had no valid word boundary to land on and the whole match
failed, so the real secret reached the redaction output unredacted.
Dropping the trailing anchors lets the body class itself stop the match,
so the credential prefix still gets redacted even when noise follows it.
Also recognize sk-admin- alongside sk-proj-/sk-svcacct- so OpenAI admin
keys aren't skipped by the narrowed modern-key branch.

Clean pruned any worktree under the caller-supplied BaseDir, but Prepare
only ever creates worktrees under a per-repository
zero-worktree-<repoKey> subtree of it. Scope pruning to that subtree so a
worktree a user manages by hand elsewhere under a shared BaseDir is never
force-removed. Also refuse to force-remove a worktree whose mtime looks
stale but that still has uncommitted or untracked changes: a task can
hold live work while waiting on a model, network, or user for longer
than the staleness window without writing to the tree again.
…laims to

activePath/internal was created by the same MkdirAll as the nested pkg
dir but never backdated, so it kept a fresh mtime and worktreeIsStale's
walk reported "not stale" as soon as it hit that directory, before ever
reaching the freshly-written file two levels deeper. The test passed
without actually exercising recursion past the first directory.
…s dirty

Prepare never called git worktree lock, so the entry.locked skip in Clean
only ever protected worktrees a human locked by hand, never zero's own; a
worktree that finished committing and sat idle (e.g. waiting on a slow
model or network retry) for more than 24h looked clean-and-stale and got
force-removed by the mtime+dirty heuristic alone. Lock every worktree
Prepare creates so it gets the same protection.

worktreeIsDirty also used git status --porcelain with no --ignored, so a
worktree holding only .gitignore-matched task data (credentials, generated
drafts, artifacts) reported as clean and got force-removed with --force,
silently discarding it. Add --ignored so those files count as dirty too.
…shed worktrees

Prepare locks every worktree it creates so Clean's mtime+dirty staleness
heuristic never force-removes one Zero is still using, but nothing ever
unlocked it, making the automatic disk-space cleanup permanently inert.

Add Release (git worktree unlock) and wire it in two ways: zero exec
--worktree defers a release once its own run finishes, since that flow's
use of the worktree is bound to its own process. zero worktrees prepare
hands the path to a longer-lived external caller with no defined
end-of-life, so a new zero worktrees release <path> subcommand lets that
caller release it explicitly when done.
…k deleted worktrees

Address CodeRabbit's review on the lock-release fix:

- zero worktrees release now resolves its path argument to absolute
  before calling Release, since git worktree unlock matches against
  the path git recorded at creation, not whatever directory the
  caller happens to be running from.
- Clean now aggregates removal failures with errors.Join instead of
  overwriting lastErr, so multiple stale worktrees failing removal in
  the same pass are all reported, not just the last one.
- Release falls back to options.Cwd as the git working directory when
  the worktree path itself no longer exists (e.g. a caller deleted a
  locked worktree by hand instead of releasing it first), so the
  orphaned lock can still be cleared.

Added regression coverage for all three.
- Prepare re-locks a reused worktree so Clean's staleness heuristic cannot
  force-remove it while the new caller is still using it; a lock already
  held by a live external caller is kept in place and reported through the
  new Result.LockAcquired field.
- exec --worktree only releases the lock its own Prepare call acquired and
  surfaces a failed release on stderr with the affected path instead of
  discarding the error.
- worktrees release wires the resolved workspace root into Options.Cwd so
  the deleted-path recovery works outside the worktree directory.
- The relative-path release test derives its expected value via
  filepath.Abs, matching the resolution the CLI uses, so macOS /var vs
  /private/var spellings no longer break it.
…d release -C

- Prepare rejects a worktree whose lock another run still holds, on both
  the reuse and the create-race paths, instead of handing a second live
  caller an unprotected shared checkout whose sole Git lock the first
  caller's exit would release.
- The automatic stale-worktree pruning runs only after the request itself
  validates, so a rejected command (an invalid --name) has no destructive
  cleanup side effect; covered end to end with real git in both directions.
- worktrees release accepts -C/--cwd naming the source repository, which
  the deleted-path recovery needs when launched outside the repo (the
  deleted worktree path is a one-way hash with no way back to its source).
git records worktree paths in physical form, so the CI runners' symlinked
(/var -> /private/var) and 8.3-short (RUNNER~1) temp spellings made Clean's
containment check skip the test's stale entry and the pruning assertion
fail on macOS and Windows.
…canonicalize base dir

Four fixes from the latest review round:

- Clean now creates a durable ref (refs/zero/orphaned-worktree/<sha>)
  for a detached worktree's HEAD before force-removing it, when that
  commit isn't already reachable from any other ref. Prepare always
  creates worktrees with `worktree add --detach`, so a commit made
  there had no ref pointing at it once the worktree was deleted,
  making it immediately eligible for git gc despite never having been
  merged/pushed elsewhere.

- Clean resolves its configured base directory through EvalSymlinks
  before comparing it against git's reported worktree paths: `git
  worktree list --porcelain` reports each worktree's PHYSICAL location
  (resolving symlink components), so a symlinked --worktree-dir made
  every worktree created under it permanently unprunable.

- Release now verifies path has a zero-worktree-<repoKey> ancestor
  directory component before running `git worktree unlock`, so it
  can't be used to clear the lock on a worktree a user or another tool
  manages by hand. The check doesn't need to know which --dir a given
  Prepare call used (nothing records that against a specific worktree,
  and the CLI never threads BaseDir through to Release) — the repoKey
  component is Prepare's actual ownership signature regardless of
  which directory it was created under.

- Route the release command's printed path and exec's release-failure
  diagnostic through the existing CLI redaction helper, and split the
  worktrees help text into prepare-specific and release-specific flag
  sections (release only ever supported -C/--cwd, not the --name/--dir/
  --json the shared block advertised).

All four have regression tests confirmed to fail without their fix
(two using real git worktrees, not just fakeRunner sequences). Build,
vet, and gofmt clean on linux/windows/darwin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rees

Two review findings on the cleanup lifecycle:

- A lock left by an abnormal exit (SIGKILL, crash, power loss) was
  skipped by Clean forever, recreating the permanent disk leak this PR
  set out to fix. exec --worktree now records its PID in the lease
  reason; Clean expires a lease whose recorded owner is provably dead,
  unlocking only after the staleness, dirty, and HEAD-preservation
  guards all pass. Human locks and PID-less leases (external
  `worktrees prepare` owners) remain permanent until explicit release,
  and any ambiguity in the liveness probe counts as alive.

- An explicitly released worktree holding only gitignored residue
  (node_modules, build output) was skipped at every age. Release is the
  owner's completion signal, so unlocked entries now block removal only
  on tracked/untracked changes; expired crashed leases keep the
  conservative --ignored probe since they never signaled completion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e safety

Split dead-lease PID checking into posix/windows implementations so
Windows can reliably tell a dead process from a live one. Fix a path
canonicalization mismatch in two release tests. Derive release
ownership from git worktree list instead of the git-dir parent, which
was wrong for repos with a separate git-dir. Refuse to clear a lock
that was not taken by Zero in the first place.
Compare Clean containment and Release ownership against physical path
spellings so macOS /var vs /private/var and symlink TMPDIR layouts match
git worktree list. Require a registered porcelain entry and a Zero lease
reason before unlock; treat an already-unlocked Zero worktree as a no-op.
- Prepare and Release now agree on repoKey regardless of which worktree
  (main or linked) Prepare runs from, by keying off git worktree list's
  first entry (always the main worktree) instead of --show-toplevel.
  A worktree prepared from a linked checkout previously failed its own
  ownership check on release and its lease could never be cleared.
- osProcessAlive on Windows no longer treats every OpenProcess failure
  as "process is dead": only ERROR_ACCESS_DENIED (a live process this
  caller lacks rights to query) is now distinguished from a genuinely
  missing PID, so Clean can no longer force-remove an active worktree
  whose owning process it simply couldn't query.
- The openai_key redaction pattern now recognizes sk-or-v1- (OpenRouter)
  alongside the existing sk-proj-/sk-svcacct-/sk-admin- prefixes, so
  hyphenated OpenAI-compatible provider keys are redacted again without
  reopening the sk-<kebab-case-phrase> false-positive this pattern was
  narrowed to avoid.
- Release/exec --worktree error text is redacted before reaching
  stderr, matching the already-redacted success path; ownership errors
  interpolate the caller-supplied path, which could carry a key-shaped
  segment.
- canonicalizePath resolves symlinks through the nearest existing
  ancestor when the target itself no longer exists, so the documented
  `release -C` recovery path works again for a worktree deleted by hand
  under a symlinked --worktree-dir.
- Prepare rolls back the worktree `git worktree add` just created if the
  subsequent lock call fails for a reason other than a concurrent
  racer, instead of leaking an unleased checkout until Clean's 24h
  staleness window reclaims it.

Not addressed here: the P2 finding that worktree ownership is provable
only by a directory-name convention plus a lock-reason prefix, both of
which a user can reproduce by hand. A durable per-worktree ownership
marker would close that gap, but internal/worktrees has been on main
since Gitlawb#70, so a marker requirement could reject worktrees an
already-installed zero created before this change existed. Needs a
decision on migration before implementing.
Address review findings that path convention plus lease-reason prefix are
forgeable by hand. Prepare now writes a zero-owner marker into the worktree
admin dir; Release and Clean require it before force-touching a path. Clean
also keys its owned subtree off the main worktree root so linked-checkout
calls still prune the same bucket Prepare uses. Tests plant the marker and
list the main worktree first so fixtures match production.
…ease rejection, and completions

- TestCleanFromLinkedWorktreePrunesStaleWorktree: pins Clean deriving its
  owned-subtree key from the main worktree root (not the invoking linked
  checkout's --show-toplevel) so Prepare/Clean run from a linked worktree
  actually reclaim the worktrees Prepare created there.
- TestReleaseRejectsForgedZeroLeaseWithoutOwnershipMarker: pins the
  ownership-marker requirement against the exact forgery jatmn described - a
  worktree under the predictable zero-worktree-<repoKey> path, manually
  locked with a reason that merely starts with the zero lease prefix.
- Fix TestPrepareCreatesDetachedGitWorktree: its fake git runner ran out of
  canned results at Prepare's post-lock ownership-marker write, so
  writeOwnershipMarker resolved gitDir to "" and os.WriteFile wrote
  "zero-owner" as a relative path into the test process's real working
  directory instead of failing loudly. Give it autoAbsoluteGitDir like the
  other Prepare-exercising tests use and assert on the marker-write call.
- completions_test.go: assert `worktrees`/`worktree` completions include
  `release` alongside `prepare`.
When Prepare reuses an existing worktree, it now touches the directory
to refresh its mtime. This prevents Clean from force-removing a
long-running but idle worktree (e.g. waiting on a model) that has no
recent file changes but is still actively in use.

Addresses the data-loss risk flagged in the PR review where mtime-only
staleness + --force removal could discard live worktrees.

Co-authored-by: cairn-code
Co-Authored-By: cairn-code <282421612+cairn-code@users.noreply.github.com>
Co-Authored-By: cairn-code <282421612+cairn-code@users.noreply.github.com>
…ot to primaryRoot, and add anthropic_key pattern

Unlock using matched porcelain entry path, set Result.RepoRoot to primaryRoot in Prepare, add dedicated anthropic_key secret pattern, and canonicalize fake-runner test paths.

Refs Gitlawb#632
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds worktree locking, release, stale cleanup, lease recovery, and CLI integration. It also expands secret detection and redaction for modern credential formats, overlapping values, suffixes, and false-positive cases.

Changes

Worktree lifecycle

Layer / File(s) Summary
Worktree preparation and ownership
internal/worktrees/*, internal/worktrees/worktrees_test.go
Prepare resolves the primary worktree, acquires locks, records lease PIDs, writes ownership markers, and reports lock ownership.
Release and stale cleanup
internal/worktrees/*, internal/worktrees/worktrees_test.go
Release validates and unlocks Zero-owned worktrees. Clean handles stale leases, deleted paths, dirty content, ownership validation, and unreachable commits.
Worktree release CLI
internal/cli/app.go, internal/cli/workflows.go, internal/cli/completions.go, internal/cli/*test.go
The CLI adds worktrees release, path and -C handling, dependency wiring, help text, completion entries, redacted errors, and confirmation output.
Exec lock ownership cleanup
internal/cli/exec.go, internal/cli/workflow_test.go
Exec records the process PID and releases only locks acquired by the current run. Release failures go to stderr.

Secret detection and redaction

Layer / File(s) Summary
Credential pattern coverage
internal/secrets/*, internal/tools/bash_secrets_test.go
Scanning adds Anthropic keys, broader OpenAI and JWT formats, longer credentials, suffix handling, and kebab-case false-positive filtering.
Overlapping secret replacement
internal/redaction/*, internal/cli/*test.go, internal/tui/*test.go, internal/zerocommands/*test.go, internal/zerogit/*test.go, internal/selfverify/*test.go, internal/sessions/*test.go, internal/verify/*test.go
Redaction sorts extra secrets by length, applies OpenAI-specific matching, and updates coverage for overlapping, suffixed, and longer credentials.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant WorktreeService
  participant Git
  participant Lease
  CLI->>WorktreeService: prepare worktree
  WorktreeService->>Git: resolve repository and worktree
  WorktreeService->>Lease: acquire lock and record PID
  WorktreeService-->>CLI: return worktree and LockAcquired
  CLI->>WorktreeService: release worktree
  WorktreeService->>Git: validate and unlock worktree
  WorktreeService-->>CLI: report release result
Loading

Possibly related PRs

  • Gitlawb/zero#632: Modifies the same worktree locking, release, CLI, and secret-scanner code.

Suggested reviewers: anandh8x, vasanthdev2004, gnam1990

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two primary changes: secret redaction fixes and stale worktree pruning.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…test

TestFormatBashOutputRedactsAnthropicKey checked for the openai_key
placeholder instead of anthropic_key, a copy-paste leftover. The
redaction itself was already correct; only the assertion was wrong.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
internal/worktrees/worktrees_posix.go (1)

20-27: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Default unknown signal errors to alive, so the fail-closed contract holds.

processAlive documents that any ambiguity counts as alive, so an uncertain answer can never expire a lease. This implementation returns alive only for nil and EPERM. Every other error, including an unexpected one, returns false and marks the lease expired. That path lets Clean unlock and force-remove a worktree based on an inconclusive probe.

Treat only the definite "no such process" answers as dead.

🛡️ Proposed fix
 	err = proc.Signal(syscall.Signal(0))
 	if err == nil {
 		return true
 	}
-	if errors.Is(err, os.ErrProcessDone) {
+	// Only ESRCH / ErrProcessDone prove the PID names no live process. Any
+	// other error is inconclusive, so report alive and keep the lease.
+	if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) {
 		return false
 	}
-	return errors.Is(err, syscall.EPERM)
+	return true
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/worktrees/worktrees_posix.go` around lines 20 - 27, Update the error
handling in processAlive so only the definite no-process result
(os.ErrProcessDone) returns false; retain true for successful probes and
permission errors, and default all other unexpected signal errors to true to
preserve the fail-closed lease behavior.
internal/cli/workflow_test.go (2)

325-325: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two secret scanners flag these synthetic key literals.

The literals are clearly fake fixtures for the redaction assertions, and the tests are valuable. Both Betterleaks and ast-grep report them as hard-coded credentials, which adds recurring noise to every scan of this file. Assemble the fixture at runtime from fragments, or add the scanners' inline allow annotation, so the finding does not have to be triaged again.

♻️ Proposed fix
-	secret := "sk-proj-abcDEF123_ghiJKL456-mnoPQR789stu"
+	// Assembled at runtime so secret scanners do not report this synthetic
+	// redaction fixture as a hard-coded credential.
+	secret := "sk-" + "proj-abcDEF123_ghiJKL456-mnoPQR789stu"

Also applies to: 933-933

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflow_test.go` at line 325, Update the synthetic secret
fixtures in the workflow tests, including both occurrences of the key literal,
so scanners no longer classify them as hard-coded credentials. Assemble each
fixture at runtime from non-secret fragments, or apply the repository-supported
inline allow annotation while preserving the existing redaction assertions.

Source: Linters/SAST tools


187-203: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use t.Chdir for the workflow test temporary directory.

go.mod declares Go 1.26.5, so testing.T.Chdir is available. Use it instead of manual os.Chdir plus deferred restore; it restores the working directory automatically during cleanup.

♻️ Proposed fix
-	origWd, err := os.Getwd()
-	if err != nil {
-		t.Fatal(err)
-	}
 	parent := t.TempDir()
 	worktreeDir := filepath.Join(parent, "task-a")
 	if err := os.Mkdir(worktreeDir, 0o700); err != nil {
 		t.Fatal(err)
 	}
-	if err := os.Chdir(parent); err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		if err := os.Chdir(origWd); err != nil {
-			t.Fatal(err)
-		}
-	}()
+	t.Chdir(parent)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflow_test.go` around lines 187 - 203, Update the workflow
test’s working-directory setup to use testing.T.Chdir with the temporary parent
directory, and remove the manual os.Getwd, os.Chdir, and deferred restoration
logic. Preserve the existing worktreeDir creation and test behavior.
internal/cli/workflows.go (1)

133-134: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Three small CLI UX inconsistencies in the release path.

  1. Line 180 prints the raw path argument, but line 177 releases absPath. A user who runs zero worktrees release task-a sees released task-a while git unlocked an absolute path. Report the path that was acted on.
  2. Line 134 does not trim the --cwd= value, while parseWorktreeCommandArgs trims the same flag at line 405. --cwd=" " then reaches resolveWorkspaceRoot as whitespace.
  3. In the help text, -h, --help is now the last entry of the release flags: block, so it reads as release-only. It applies to both subcommands.
♻️ Proposed fixes
 		case strings.HasPrefix(arg, "--cwd="):
-			cwdFlag = strings.TrimPrefix(arg, "--cwd=")
+			cwdFlag = strings.TrimSpace(strings.TrimPrefix(arg, "--cwd="))
-	if _, err := fmt.Fprintf(stdout, "released %s\n", redactCLIString(path)); err != nil {
+	if _, err := fmt.Fprintf(stdout, "released %s\n", redactCLIString(absPath)); err != nil {
 		return exitCrash
 	}
 release flags:
   -C, --cwd <path>        Source repository directory (required if the
                            worktree directory was already deleted)
+
+common flags:
   -h, --help              Show this help

Also applies to: 180-182

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/workflows.go` around lines 133 - 134, Update the release
workflow to report the acted-on absolute path variable (absPath) instead of the
raw path argument, trim whitespace from the --cwd= value when assigning cwdFlag
in argument parsing, and move the -h, --help entry in the release flags help
text so it clearly applies to both release subcommands.
internal/worktrees/worktrees_test.go (1)

1268-1272: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two doc comments describe tests that are not below them.

At lines 1268-1272 a comment about --ignored blocking force-removal sits directly above TestCleanPreservesUnreachableCommitBeforeRemoval, which tests commit preservation. At lines 1547-1553 a comment about worktreeIsDirty and --ignored sits above TestCleanHonorsTouchLiveness. It looks like the tests those paragraphs belonged to moved. Move each paragraph onto its own test or delete it, so the next reader does not trust the wrong description.

Also applies to: 1547-1553

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/worktrees/worktrees_test.go` around lines 1268 - 1272, Correct the
misplaced doc comments in internal/worktrees/worktrees_test.go: move the
--ignored/worktreeIsDirty force-removal explanation to the test that verifies
ignored-only content is treated as dirty, and move the commit-preservation
explanation to TestCleanPreservesUnreachableCommitBeforeRemoval. Ensure each
paragraph directly precedes the test it describes, or remove it if no matching
test exists.
internal/worktrees/worktrees.go (1)

284-294: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use a locale-stable signal for git worktree lock collisions.

lockWorktree treats already locked as a non-lock failure, but that message is translated under LANG/LC_MESSAGES. A localized message can cause Prepare to classify a live worktree collision as a real failure and roll back/remove the newly created worktree. Run the lock through defaultRunGit with LC_ALL=C, or use a locale-independent signal such as the lock exit code plus another stable Git state (for example, querying the worktree list before running add).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/worktrees/worktrees.go` around lines 284 - 294, Update lockWorktree
to classify existing-lock collisions using a locale-stable signal instead of
matching the localized “already locked” text. Run git worktree lock through
defaultRunGit with LC_ALL=C, or use the lock exit code together with another
stable Git-state check, while preserving successful locks and genuine error
propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/redaction/redaction.go`:
- Around line 160-168: Update the textSecretPatterns used by RedactString to
match secrets.Scan’s end-boundary behavior and supported secret formats,
removing trailing \b behavior that misses appended suffixes or leaves terminal
hyphens after backtracking. Add parity tests exercising RedactString with
appended suffixes and secrets ending in hyphens.

In `@internal/tools/bash_secrets_test.go`:
- Around line 28-36: Update TestFormatBashOutputRedactsAnthropicKey to store the
complete Anthropic key fixture in a key variable, pass it into formatBashOutput,
and assert that the resulting output does not contain the full key while
retaining the typed redaction-placeholder assertion.

In `@internal/worktrees/worktrees_test.go`:
- Around line 1474-1481: Update the Clean-related fixtures, including the test
covering ignored data and TestCleanSkipsWorktreeWithRecentNestedActivity,
TestCleanHonorsLiveLease, TestCleanHonorsTouchLiveness, and
TestCleanSkipsDirtyStaleWorktree, to list the main worktree rooted at repoRoot
before the target entry, matching TestCleanRecoversExpiredLease. Pass tempDir
through physicalTestPath where required, and add positive assertions for the
expected git call sequence so the tests genuinely reach the lease, status, and
removal logic.

---

Nitpick comments:
In `@internal/cli/workflow_test.go`:
- Line 325: Update the synthetic secret fixtures in the workflow tests,
including both occurrences of the key literal, so scanners no longer classify
them as hard-coded credentials. Assemble each fixture at runtime from non-secret
fragments, or apply the repository-supported inline allow annotation while
preserving the existing redaction assertions.
- Around line 187-203: Update the workflow test’s working-directory setup to use
testing.T.Chdir with the temporary parent directory, and remove the manual
os.Getwd, os.Chdir, and deferred restoration logic. Preserve the existing
worktreeDir creation and test behavior.

In `@internal/cli/workflows.go`:
- Around line 133-134: Update the release workflow to report the acted-on
absolute path variable (absPath) instead of the raw path argument, trim
whitespace from the --cwd= value when assigning cwdFlag in argument parsing, and
move the -h, --help entry in the release flags help text so it clearly applies
to both release subcommands.

In `@internal/worktrees/worktrees_posix.go`:
- Around line 20-27: Update the error handling in processAlive so only the
definite no-process result (os.ErrProcessDone) returns false; retain true for
successful probes and permission errors, and default all other unexpected signal
errors to true to preserve the fail-closed lease behavior.

In `@internal/worktrees/worktrees_test.go`:
- Around line 1268-1272: Correct the misplaced doc comments in
internal/worktrees/worktrees_test.go: move the --ignored/worktreeIsDirty
force-removal explanation to the test that verifies ignored-only content is
treated as dirty, and move the commit-preservation explanation to
TestCleanPreservesUnreachableCommitBeforeRemoval. Ensure each paragraph directly
precedes the test it describes, or remove it if no matching test exists.

In `@internal/worktrees/worktrees.go`:
- Around line 284-294: Update lockWorktree to classify existing-lock collisions
using a locale-stable signal instead of matching the localized “already locked”
text. Run git worktree lock through defaultRunGit with LC_ALL=C, or use the lock
exit code together with another stable Git-state check, while preserving
successful locks and genuine error propagation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 06ea9d09-39af-4492-995c-e3afd7f986f2

📥 Commits

Reviewing files that changed from the base of the PR and between 8e26679 and 29f1d87.

📒 Files selected for processing (16)
  • internal/cli/app.go
  • internal/cli/completions.go
  • internal/cli/completions_test.go
  • internal/cli/exec.go
  • internal/cli/workflow_test.go
  • internal/cli/workflows.go
  • internal/redaction/audit_fixes_test.go
  • internal/redaction/redaction.go
  • internal/secrets/scanner.go
  • internal/secrets/scanner_test.go
  • internal/tools/bash_secrets_test.go
  • internal/worktrees/worktrees.go
  • internal/worktrees/worktrees_posix.go
  • internal/worktrees/worktrees_test.go
  • internal/worktrees/worktrees_windows.go
  • internal/worktrees/worktrees_windows_test.go

Comment thread internal/redaction/redaction.go
Comment thread internal/tools/bash_secrets_test.go
Comment thread internal/worktrees/worktrees_test.go
Align RedactString token boundaries with secrets.Scan, strengthen the
Anthropic bash redaction assertion, fix Clean fixtures so they reach
the guards under test, and fail closed on ambiguous processAlive probes.
@euxaristia

Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit findings in c9a347a:

  1. RedactString parity with secrets.Scan — removed trailing \b anchors; aligned formats/boundaries; added suffix and trailing-hyphen parity tests.
  2. Anthropic bash test — asserts the full key is absent, not only the prefix.
  3. Clean fixtures — list main worktree first, use physicalTestPath, and positively assert the git call sequence reaches the guard under test.
  4. processAlive — only ESRCH/ErrProcessDone count as dead; ambiguous errors keep the lease.
  5. CLI release path: print absPath, trim --cwd=, help text for -h.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

@euxaristia I will review the complete PR, including the changes in c9a347a.

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 59 minutes.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 1, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. The worktree lease/ownership work is genuinely good — the marker-in-the-git-admin-dir trick is the right ownership proof, the fail-closed staleness and dirty probes are careful, and I mutation-tested a couple of the guards (deleting the ownership-marker rejection and forcing includeIgnored=false both turn the right tests red, so they're real). But two things need fixing before this can land: the redaction change is a net loss of coverage relative to main, and the new auto-Clean destroys data on upgrade.

The openai_key pattern got narrower, not wider

internal/secrets/scanner.go:49 (and its mirror at internal/redaction/redaction.go:79) went from

\bsk-[A-Za-z0-9_-]{20,}

to

\bsk-(?:proj-|svcacct-|admin-|or-v1-)[A-Za-z0-9_-]{20,}|\bsk-[A-Za-z0-9]{20,}

The fallback branch no longer allows - or _, so any sk-<vendor>-<body> key whose vendor segment isn't one of the four you enumerated matches nothing at all. I drove this through the real entry point (formatBashOutputsecrets.Redact) with sentinel tokens:

sk-fw-SENTINEL_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa        -> leaked verbatim
sk-live-SENTINELaaaaaaaaaa_bbbbbbbbbb-cccc           -> leaked verbatim

Both are matched by main's regex. Same result through redaction.RedactString; I compiled origin/main:internal/redaction/redaction.go standalone and ran the identical inputs — main returns value: [REDACTED] for both.

What makes this blocking rather than a judgement call is internal/cli/sandbox_check_test.go:155. TestRunSandboxCheckMatchedGrantRedactsReason already existed on main and pinned that a grant reason containing sk-test-secret1234567890 never reaches zero sandbox check --json. This PR rewrites that fixture to sk-proj-testsecret1234567890ab. On this branch:

RedactString("approved with sk-test-secret1234567890", Options{})
  == "approved with sk-test-secret1234567890"

So the fixture was changed because the behaviour regressed, and the test that would have caught it can't anymore. Please restore the original fixture.

I understand the motivation — TestScanIgnoresKebabCaseStartingWithSk shows sk-learn-machine-learning-model was a false positive on main. But you can kill that without giving up the general branch. RE2 has no lookahead, so do it in Go: keep \bsk-[A-Za-z0-9_-]{20,} and drop an openai_key match whose body contains no digit. sk-learn-machine-learning-model has none; every real key does. Tighten further if you want, but the enumerated-prefix approach means every new vendor prefix is a silent miss until someone notices.

Same file, internal/redaction/redaction.go:87: the JWT pattern now requires the second segment to start with eyJ. That's fine for a standard JWS but not for a compact JWS with a non-JSON payload or for a JWE, where segment two is an encrypted key. Probed:

eyJhbGciOiJIUzI1NiJ9.U0VOVElORUxwYXlsb2Fk.SENTINELsignature123
  main: [REDACTED]      this branch: leaked in full
5-part JWE
  main: [REDACTED].SENTINELciphertext.SENTINELtag12345    this branch: leaked in full

Please keep the looser alternative alongside the strict one.

Clean destroys pre-upgrade worktrees' gitignored contents

internal/worktrees/worktrees.go:858 runs worktreeIsDirty(..., expiredLease) before the ownership/legacy determination at :871-880. For a legacy entry — unlocked, no marker, created by the version of Prepare that's on main today — expiredLease is false, so the probe omits --ignored and the worktree is treated as explicitly released. But release didn't exist in the version that created it; unlocked was the only state there ever was.

Reproduced end to end with real git: created a worktree exactly the way main's Prepare does (git worktree add --detach under <base>/zero-worktree-<key>/legacy-task, no lock, no marker), dropped a .gitignored .env in it, asserted git status --porcelain is clean and --ignored is not, aged everything 48h, ran Clean(24h). The worktree and the .env are both gone. And Prepare auto-invokes Clean at worktrees.go:86-88, which I also confirmed removes a pre-existing legacy worktree — so this fires on the very first zero worktrees prepare or zero exec --worktree after upgrade, with no warning. Clean doesn't exist on main at all, so this is entirely introduced by this PR.

Fix is a reorder: determine ownership/legacy first, then pass includeIgnored = expiredLease || migratedLegacy. A worktree that predates the release protocol should get the same benefit of the doubt as a crashed lease.

Related: internal/worktrees/worktrees_test.go:1935 annotates its third fakeRunner result // status --porcelain --ignored: clean, but the command actually issued on that path is git status --porcelain — I added a runner.commandLine(2) assertion to the same fixture to confirm. The test asserts nothing about it, so nothing pins this either way.

Smaller things I'd want fixed in the same pass

internal/worktrees/worktrees_windows.go:39openProcessErrorMeansAlive returns alive only for ERROR_ACCESS_DENIED; everything else reads as dead. Probed:

ERROR_ACCESS_DENIED         -> alive=true
ERROR_INVALID_PARAMETER     -> alive=false
ERROR_NOT_ENOUGH_MEMORY     -> alive=false
ERROR_NO_SYSTEM_RESOURCES   -> alive=false
ERROR_TOO_MANY_OPEN_FILES   -> alive=false

processAlive's own doc at worktrees.go:206-212 promises the opposite ("any ambiguity ... counts as alive"), and the POSIX sibling honours it. Under handle or memory pressure a live run's lease reads as expired and Clean can unlock and worktree remove --force the workspace out from under it. return !errors.Is(err, windows.ERROR_INVALID_PARAMETER) restores the contract.

internal/cli/workflows.go:171runWorktreesRelease carefully redacts the release error and the success line, but forwards resolveWorkspaceRoot's error (and filepath.Abs's at :155) verbatim:

$ zero worktrees release -C /no/such/sk-proj-abcDEF123_ghiJKL456-mnoPQR789stu <path>
[zero] cwd must be an existing directory: ...\no\such\sk-proj-abcDEF123_ghiJKL456-mnoPQR789stu

Wrap both in redactCLIString.

internal/worktrees/worktrees.go:153 — the reuse path never reclaims a dead-owner lease even though Clean knows how at :860-865. Probed: Prepare with a dead LeasePID, then Prepare again with the same name and a live PID, and the second call fails with "locked by another active run". So a SIGKILLed zero exec --worktree --worktree-name X bricks that name until someone runs release by hand. The error message is actionable so this isn't severe, but the asymmetry is worth closing: if leasePID(reason) names a dead process, unlock and retry.

Notes, no action needed

  • Result.LockAcquired's comment says it's false when a reused worktree is already locked by another live caller, but that case returns an error at :157/:217, so it's never observably false. The if preparedWorktree.LockAcquired guard at internal/cli/exec.go:222 is dead in practice — either make the reuse path really return false, or simplify both.
  • refs/zero/orphaned-worktree/<sha> accumulates with nothing pruning it, which also pins the objects forever.
  • Prepare skips the auto-Clean when options.RunGit != nil, which is exactly what every unit test sets, so that wiring has no coverage — I needed a real-git probe to see it.
  • TestBuildServeScopeKeepsLexicalPaths fails on my box for lack of symlink privilege; pre-existing and unrelated. Everything else in internal/secrets, internal/redaction, internal/worktrees, internal/tools and internal/cli passes, and go build ./... / go vet are clean.

Unrelated to the code: I approved #857 earlier today and meant what I said there. Two of the things below are the classes that section names — a test fixture adjusted to fit a narrowed pattern, and a claim in the description that's wider than what shipped — so this is me applying your own list, not moving goalposts. Everything I found is above in one pass; there's no second round waiting behind it.

…safety

Address human review on Gitlawb#855: keep sk- bodies with a digit filter instead
of enumerated vendor prefixes, add a looser JWT form, restore the
sk-test fixture, probe legacy ownership before dirty, treat non-INVALID
Windows OpenProcess errors as alive, redact Abs/cwd release errors, and
reclaim dead-owner leases on Prepare reuse.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/redaction/redaction.go`:
- Around line 71-74: Update the redaction logic around openaiKeyPattern and
secretMatchHasDigit so known OpenAI prefixes such as sk-proj-, sk-svcacct-, and
sk-admin- are redacted even when the token contains no digits, while retaining
the digit requirement for unknown sk- forms. Add a regression case covering an
alphabet-only sk-proj- token.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a571c6c5-c539-40ba-86b2-e95d54bb3bd2

📥 Commits

Reviewing files that changed from the base of the PR and between c9a347a and 7be0e6f.

📒 Files selected for processing (26)
  • internal/cli/backends_test.go
  • internal/cli/extensions_test.go
  • internal/cli/hooks_manage_test.go
  • internal/cli/mcp_commands_test.go
  • internal/cli/workflow_test.go
  • internal/cli/workflows.go
  • internal/redaction/audit_fixes_test.go
  • internal/redaction/redaction.go
  • internal/redaction/redaction_test.go
  • internal/secrets/boundary_test.go
  • internal/secrets/scanner.go
  • internal/secrets/scanner_test.go
  • internal/selfverify/contracts_test.go
  • internal/selfverify/selfverify_test.go
  • internal/sessions/replay_test.go
  • internal/verify/contracts_test.go
  • internal/verify/verify_test.go
  • internal/worktrees/worktrees.go
  • internal/worktrees/worktrees_test.go
  • internal/worktrees/worktrees_windows.go
  • internal/worktrees/worktrees_windows_test.go
  • internal/zerocommands/backend_doctor_test.go
  • internal/zerocommands/backend_snapshots_test.go
  • internal/zerocommands/contracts_test.go
  • internal/zerogit/contracts_test.go
  • internal/zerogit/zerogit_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • internal/cli/workflow_test.go
  • internal/secrets/scanner.go
  • internal/worktrees/worktrees_test.go
  • internal/cli/workflows.go
  • internal/worktrees/worktrees_windows.go
  • internal/worktrees/worktrees.go

Comment thread internal/redaction/redaction.go Outdated
Alphabet-only sk-proj-/sk-svcacct-/sk-admin- tokens are still credentials;
keep the digit filter only for unknown sk- vendor forms so kebab phrases
like sk-learn-… stay un-redacted.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/secrets/scanner_test.go (1)

149-170: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Require complete secret replacement in all new redaction assertions.

These tests can pass when a matcher removes only one fragment. Require the complete credential or JWT to be absent and the expected replacement marker to be present.

  • internal/secrets/scanner_test.go#L149-L170: assert full compact JWS/JWE replacement, including payload, signature, ciphertext, and tag.
  • internal/secrets/scanner_test.go#L287-L303: assert Redact("token="+key) equals "token=[REDACTED:openai_key]".
  • internal/redaction/audit_fixes_test.go#L164-L174: assert RedactString("token="+secret, Options{}) equals "token="+RedactedSecret.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/secrets/scanner_test.go` around lines 149 - 170, Strengthen the
redaction assertions across all listed sites: in TestScanDetectsLooseJWTForms at
internal/secrets/scanner_test.go:149-170, require the complete compact JWS/JWE
token—including payload, signature, ciphertext, and tag—to be absent and the
expected replacement marker to be present; at
internal/secrets/scanner_test.go:287-303, require Redact("token="+key) to equal
"token=[REDACTED:openai_key]"; and at
internal/redaction/audit_fixes_test.go:164-174, require
RedactString("token="+secret, Options{}) to equal "token="+RedactedSecret.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/secrets/scanner_test.go`:
- Around line 149-170: Strengthen the redaction assertions across all listed
sites: in TestScanDetectsLooseJWTForms at
internal/secrets/scanner_test.go:149-170, require the complete compact JWS/JWE
token—including payload, signature, ciphertext, and tag—to be absent and the
expected replacement marker to be present; at
internal/secrets/scanner_test.go:287-303, require Redact("token="+key) to equal
"token=[REDACTED:openai_key]"; and at
internal/redaction/audit_fixes_test.go:164-174, require
RedactString("token="+secret, Options{}) to equal "token="+RedactedSecret.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c70f92e-c56d-4c0a-bbf2-3386a9e85c9e

📥 Commits

Reviewing files that changed from the base of the PR and between 7be0e6f and ebc83b7.

📒 Files selected for processing (4)
  • internal/redaction/audit_fixes_test.go
  • internal/redaction/redaction.go
  • internal/secrets/scanner.go
  • internal/secrets/scanner_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/secrets/scanner.go
  • internal/redaction/redaction.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 1, 2026
@euxaristia

Copy link
Copy Markdown
Contributor Author

Review items 1-6 are on tip (7be0e6fc + ebc83b75): broad sk- with digit filter + known prefixes always redact; looser JWT alternative; legacy Clean uses includeIgnored; Windows processAlive fail-closed; worktrees release redacts cwd errors; Prepare reclaims dead-owner leases.

Ready for re-review.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — most of this is genuinely fixed, and the worktrees half I have no complaints about. Two things are still open on the secrets side, and they're the same two.

How I checked the redaction change: I built a differential harness and ran the same sentinel corpus through secrets.Redact and redaction.RedactString on origin/main (8e26679) and on this head (ebc83b7), then diffed the two outputs key by key. That's the only way to tell a widening from a narrowing when both are happening in the same patch.

1. Broadening the pattern fixed most of it, but one shape still leaks.

The good news first — everything the previous head dropped is back: sk-proj-, sk-svcacct-, sk-admin-, sk-or-v1-, sk-fw-, and plain digit-bearing sk-. And the false positives genuinely improved: sk-learn-machine-learning-model-pipeline, sk-this-is-a-normal-kebab-case-identifier and /home/user/projects/sk-utils-library-v2/main.go were all redacted on main and are correctly left alone here. That's a real win.

What the digit filter costs: a digit-free sk- body now passes straight through, both in Scan and in RedactString.

in:  prefix sk-AAAAAA…(48 A's) suffix
main: prefix [REDACTED:openai_key] suffix
head: prefix sk-AAAAAA…(48 A's) suffix

Legacy OpenAI keys are sk- + 48 base62 characters, so about one in 4,500 of them contains no digit at all — (52/62)^48. That's rare, but it's a silent full-credential leak, not a truncation, and it's the exact failure mode this PR set out to close.

The filter is aimed at kebab phrases, and kebab phrases have hyphens. Real legacy keys don't. So gate on that instead:

if p.typ == "openai_key" && !knownOpenAIKeyPrefix(m) && !containsDigit(m) &&
    strings.Contains(strings.TrimPrefix(m, "sk-"), "-") {
    continue
}

I applied that and re-ran the corpus: the two digit-free legacy shapes come back, and nothing else moves — every kebab false positive above stays un-redacted. Mirror it in redaction.go's secretMatchHasDigit twin.

2. The fixture is still weakened, and it's what's hiding item 1.

internal/secrets/boundary_test.go still differs from main. I checked out main's copy of the file onto this head and ran it:

boundary_test.go:35: real secret NOT caught in "token: \"sk-abcdefghijklmnopqrstuvwxyz\""

That's the same shape as the leak above. Appending 0 to the fixture is what made the test agree with the new pattern rather than the other way round — which is what I asked us not to do last time. With the hyphen gate applied, main's original fixture passes unmodified, so nothing needs changing there at all.

While I was in it, I probed the other three fixture edits individually. All three pass with main's original values:

findings=1 in="export OPENAI_API_KEY=sk-proj-abcdefghijklmnopqrstuvwx"
findings=1 in="key is sk-svcacct-abcdefghijklmnopqrstuvwx at the end"
findings=1 in="    secret_test.go:12: token sk-proj-abcdefghijklmnopqrstuvwxyz"

Those are known prefixes, so the digit filter never applied to them. The 0s in boundary_test.go, redaction_test.go and audit_fixes_test.go are churn — please revert them so the diff shows only the one fixture that genuinely had to move (and after the fix, none of them do).

One thing that isn't in my original list but should be in the PR body. Aligning RedactString to secrets.Scan also raised its floors relative to main: github_pat 12→22, gh[pousr]_ 12→36 with _ dropped from the body class, AIza 12→35, sk- 12→20 with . dropped, JWT segments 8→10. Each of those is a shape main redacted and this head doesn't — e.g. ghp_abcd_efghijklmnopqrstuvwxyz0123456789 now passes through whole. I'm fine with all of it, since the new floors match real issued key lengths and a single source of truth is the right call. But it arrived unannounced inside a fix for the opposite problem, and that's how a narrowing slips past review. Please say so in the summary.

3, 4, 5, 6, 7, 8 — all closed. I mutation-tested each one I'm accepting:

  • Ordering ownership/legacy detection ahead of the dirty probe is right, and includeIgnored := expiredLease || legacy is load-bearing: dropping || legacy turns both legacy tests red with the correct message.
  • firstStatusCommand() is a real assertion now, and it fails under the same mutation.
  • JWT: the loose alternative does what it should. A non-JSON-payload JWS is detected where main detected nothing, and a five-segment JWE now has its header, encrypted CEK and IV redacted. Ciphertext and tag survive, which I'm happy with — without the CEK they're inert.
  • openProcessErrorMeansAlive is correctly fail-closed. Reverting it to errors.Is(err, ERROR_ACCESS_DENIED) fails TestOpenProcessErrorMeansAliveOnAmbiguousErrors. Good call including TestOsProcessAliveReportsDeadAfterExit against a real exited process rather than only errno constants — that distinction has bitten us on Windows before.
  • Release error redaction: I ran -C <bad>, --cwd=<bad>, the releaseWorktree failure and the success echo through the real CLI entry point. All four redact.
  • Dead-owner reclaim: stubbing the reclaim branch to if false turns TestPrepareReclaimsDeadOwnerLeaseOnReuse red.

One gap on 8. Nothing tests the safety direction — that a lease held by a live pid is still rejected and never unlocked. TestPrepareRejectsWorktreeLockedByAnotherRun doesn't get there: its fakeRunner runs out of queued results, so reclaimDeadOwnerLease sees an empty worktree list and returns early. It never reaches the processAlive(pid) branch. I wrote that test locally and it passes, so this is about pinning behaviour, not a bug — but over-reclaiming would hand one checkout to two live runs, which is worse than the bug we started with. Please add it.

Two nits, take or leave: the strict jwt pattern is a strict subset of the loose one, so the first entry is dead code; and unknown worktrees release flag %q echoes the raw argument unredacted, though that matches what every other command in main already does.

Fix 1 and 2 and I'll approve. Everything else here is solid work.

Local run: go test ./internal/worktrees/... ./internal/secrets/... ./internal/redaction/... ./internal/cli/... ./internal/tools/... green apart from TestBuildServeScopeKeepsLexicalPaths, which needs symlink privileges and fails on my box regardless of branch. gofmt clean.

Everything above is one pass — six of the eight are genuinely closed and I've said which. The two that remain are really one thing: the digit filter and the fixture that hides it. The hyphen gate closes both, and then main's original fixture passes untouched, so this should be a small change rather than another round of pattern tuning.

@Vasanthdev2004

Vasanthdev2004 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@euxaristia one thing from the #855 review I want to pull out on its own, because it's easy to lose in a list of eight and it's the bit that actually matters.

The fixture in internal/secrets/boundary_test.go moved again. Last round a 0 was appended to sk-abcdefghijklmnopqrstuvwxyz so it matched the narrowed pattern; this round it's still carrying that 0. I'm not raising it as a process complaint I'm raising it because it's the mechanism by which the leak survived. Check out main's copy of that file onto your head and it fails with "real secret NOT caught", on exactly the shape that still leaks. The test was the only thing standing between that pattern and review, and adjusting it to agree made the pattern look correct.

That's a genuinely easy trap. When a test fails after a pattern change, the fixture is usually the fastest thing to move, and it's often right to move it. Here it happened to be the alarm rather than the noise.

Worth saying: the other three fixture edits I asked about turned out to be unnecessary sk-proj-, sk-svcacct- and the audit one all pass with main's original values, because the digit filter never applied to known prefixes. So the diff only ever needed the one fixture to move, and with the hyphen gate it doesn't need that either.

None of this changes how the week has gone from my side. You've turned around seven PRs in a day and taken the harder fix over the easy one more than once the beforeTool veto on #853 being the clearest case, where I'd offered you the cheap option and you closed the gap properly instead. This is one line in one file, not a pattern I'm worried about.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/tui/command_output_test.go (1)

101-114: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep the original regression inputs and verify complete secret removal.

All changed assertions reject only the complete new value. A prefix-only replacement can leave a credential tail while these tests pass.

  • internal/tui/command_output_test.go#L101-L114: retain the shorter Google-token case and assert that no token suffix remains.
  • internal/tui/command_output_test.go#L190-L198: apply the same complete-removal check to formatted command output.
  • internal/tui/command_polish_test.go#L256-L256: retain the previous shorter sandbox-grant fixture.
  • internal/tui/command_polish_test.go#L315-L315: reject both the complete credential and any leaked suffix.
  • internal/tui/model_test.go#L375-L375: retain the previous shorter sandbox-grant fixture.
  • internal/tui/model_test.go#L409-L409: reject both the complete credential and any leaked suffix.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/command_output_test.go` around lines 101 - 114, Preserve the
original shorter regression fixtures and strengthen complete-removal assertions:
in internal/tui/command_output_test.go lines 101-114, retain the shorter
Google-token case and reject its leaked suffix; in lines 190-198, apply the same
suffix check to formatted command output; retain the previous shorter
sandbox-grant fixtures in internal/tui/command_polish_test.go line 256 and
internal/tui/model_test.go line 375; update internal/tui/command_polish_test.go
line 315 and internal/tui/model_test.go line 409 to reject both each complete
credential and any leaked suffix.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/tui/command_output_test.go`:
- Around line 101-114: Preserve the original shorter regression fixtures and
strengthen complete-removal assertions: in internal/tui/command_output_test.go
lines 101-114, retain the shorter Google-token case and reject its leaked
suffix; in lines 190-198, apply the same suffix check to formatted command
output; retain the previous shorter sandbox-grant fixtures in
internal/tui/command_polish_test.go line 256 and internal/tui/model_test.go line
375; update internal/tui/command_polish_test.go line 315 and
internal/tui/model_test.go line 409 to reject both each complete credential and
any leaked suffix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6790eed6-418f-4af2-8f35-fc89b6f03afc

📥 Commits

Reviewing files that changed from the base of the PR and between ebc83b7 and d2030d6.

📒 Files selected for processing (7)
  • internal/redaction/audit_fixes_test.go
  • internal/redaction/redaction.go
  • internal/secrets/scanner.go
  • internal/tui/command_output_test.go
  • internal/tui/command_polish_test.go
  • internal/tui/model_test.go
  • internal/worktrees/worktrees_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/secrets/scanner.go
  • internal/worktrees/worktrees_test.go
  • internal/redaction/redaction.go
  • internal/redaction/audit_fixes_test.go

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.

2 participants