feat: Antigravity + Cursor CLI support, cross-platform & secret-safety fixes (v0.2.0)#7
Conversation
…r bugs Prepares the v0.2.0 release. Builds on the just-merged opencode/GitHub Copilot CLI/Kilo Code adapters (PR #5) and community-health files (PR #6). New tool support - Antigravity (Google): a three-root IDE adapter — the VS Code-style User dir (settings/keybindings/snippets), the ~/.antigravity extension list, and the shared ~/.gemini agent config (GEMINI.md, settings.json, MCP servers, skills). Capture is allowlist-based and the denylist hard-excludes the Google OAuth tokens that live in ~/.gemini (oauth_creds.json, google_accounts.json, *.pb, history) so no credential can reach the repo by construction. - Cursor CLI (cursor-agent): it SHARES ~/.cursor with the IDE, so instead of a duplicate adapter the existing Cursor adapter is widened to also capture cli-config.json + global commands/ + rules/, honor CURSOR_CONFIG_DIR, and exclude the CLI's runtime worktrees. Cross-platform fixes (from the PR #5 review, verified against official docs) - Windows: opencode + Kilo config now resolves to ~/.config/<tool> (XDG on every OS) instead of %APPDATA%\<tool>. The old mapping made detect() report the tool absent and restore write where the CLI never reads. - Copilot: freeze the real user config (settings.json, mcp-config.json, lsp-config.json, copilot-instructions.md, instructions/, agents/, hooks/) and hard-deny config.json — it is auto-managed auth/plugin state, and restoring a stale one would clobber the target machine's sign-in. - Kilo: also capture tui.json(c) (terminal-UI settings) so they round-trip. Tests: +25 (200 total). Adds Antigravity capture/restore + secret-safety (the OAuth token reaches no captured file), Copilot config-in / auth-out, Kilo TUI, a Windows-path regression (faked win32), and per-OS Antigravity User-dir resolution (macOS Library / Linux .config / Windows APPDATA). Antigravity is wired through the full stack: types + TOOL_IDS, adapter registry, os.ts (toolHomeDir/installCommandFor/cliBinaryName), platform/install dependencies, config + manifest schemas, sanitizer denylist, secret-file map, and every command (backup, status, restore incl. multi-root frozenRootsForTool, init, setup, secrets). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACwssULEcxKCbB28zZRZ63
…dening) A security review found a HIGH-severity gap: the shared "contains a NUL byte => binary" heuristic base64'd a file straight into the repo, skipping the sanitizer entirely — even with includeSecrets=false. A UTF-16-encoded JSON config (PowerShell's default Out-File/Set-Content encoding on Windows) packs a 0x00 after every ASCII char, so a secret-bearing settings.json / mcp_config.json would have been committed unredacted. - Add src/utils/capture-bytes.ts: decodeForCapture() detects UTF-16 (by BOM or a NUL-interleave heuristic) and decodes it to text so the normal sanitize + template path always runs; genuinely-binary content is returned as binary plus a lossy UTF-8 view for a fail-safe scan. - Route all four capture sites through it (antigravity, shared/configDir for opencode/Copilot/Kilo, cursor, claude). In the binary branch, when not opted into carrying secrets, DROP any file whose bytes contain secret-shaped content (with a warning) rather than storing it raw. - Test: a UTF-16LE (no-BOM) opencode.json carrying an MCP API key is captured as sanitized TEXT with the secret redacted, and the token reaches no file. 202 tests pass. The review's remaining LOW items (unsanitized symlink-target strings, heuristic-only redaction of hook scripts, case-sensitive denylist) are pre-existing, defense-in-depth-only, and left as documented follow-ups — the allowlist remains the primary guarantee for the OAuth files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACwssULEcxKCbB28zZRZ63
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughThis PR adds a new Antigravity adapter (capture, restore, detect, install-check) with multi-root filesystem handling, wires the tool through registry, CLI commands, schemas, types, and platform/install logic, updates denylists and frozen-path allowlists for several adapters, and introduces a shared UTF-16-aware byte decoder replacing NUL-byte binary detection across capture pipelines. ChangesAntigravity Tool Integration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant CLI as arbella backup CLI
participant Adapter as antigravityAdapter
participant FS as Filesystem Roots
participant Sanitizer as Sanitizer
participant Repo as Backup Repo
CLI->>Adapter: capture(ctx)
Adapter->>FS: check antigravity/user/gemini roots present
FS-->>Adapter: root existence + frozen files
Adapter->>Adapter: walkFrozen (denylist filter, symlinks, files)
Adapter->>Sanitizer: sanitizeText / detect secret-shaped bytes
Sanitizer-->>Adapter: redacted content + SecretRefs
Adapter->>Adapter: template content with vars
Adapter-->>Repo: CapturedFile entries (text/base64)
Adapter-->>CLI: CaptureResult (files, warnings, secrets)
sequenceDiagram
participant CLI as arbella pull CLI
participant Adapter as antigravityAdapter
participant Plan as planActions
participant FS as Destination Roots
CLI->>Adapter: restore(ctx, data)
Adapter->>Plan: planActions(ctx, data) [dry-run]
Plan->>FS: check existing destinations (sourceOfTruth=local)
FS-->>Plan: existence status
Plan-->>Adapter: RestoreAction[] (write/symlink/skip)
Adapter->>FS: writeRestoredFile / writeRestoredSymlink
FS-->>Adapter: write result or warning
Adapter-->>CLI: restore complete
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9c560d7339
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Prepares the v0.2.0 release by expanding adapter coverage (Antigravity + Cursor CLI), tightening cross-platform config-dir resolution (notably Windows), and hardening capture sanitization to prevent UTF‑16 text from bypassing secret redaction.
Changes:
- Add Google Antigravity adapter (multi-root capture/restore) and widen Cursor adapter to include
cursor-agentCLI config while excluding runtime worktrees. - Fix cross-platform home resolution for config-dir tools (XDG
~/.configon all OSes) and tighten Copilot capture to excludeconfig.jsonauth/internal state. - Replace NUL-byte “binary” heuristics with
decodeForCapture()to correctly decode UTF‑16 as text for sanitization, plus a fail-safe secret scan for true binary blobs.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/unit/new-tool-platform.test.ts | Extends platform/path/denylist regression coverage (Antigravity tool id + Windows XDG config-dir behavior + Copilot hard-deny). |
| test/unit/cursor-cli-and-antigravity-wiring.test.ts | Adds wiring tests for Cursor CLI frozen paths/denylist and Antigravity OS/home/install wiring. |
| test/unit/configdir-adapters.test.ts | Adds capture tests for Copilot “user config in, auth out”, Kilo TUI config capture, and UTF‑16 sanitization regression. |
| test/unit/antigravity-adapter.test.ts | Adds end-to-end unit tests for Antigravity multi-root capture/restore and OAuth “never captured” guarantees. |
| src/utils/capture-bytes.ts | Introduces decodeForCapture() to classify/decode bytes (UTF‑16 vs binary) for safe sanitization. |
| src/types.ts | Adds antigravity to ToolId and canonical TOOL_IDS order. |
| src/platform/os.ts | Unifies XDG config-dir resolution across OSes; adds CURSOR_CONFIG_DIR support; wires Antigravity home/install/binary name. |
| src/platform/install.ts | Registers Antigravity as a dependency with “none” install kind and download guidance. |
| src/core/secrets/index.ts | Adds Antigravity entry for secrets guidance (no portable credential file). |
| src/core/sanitizer/denylist.ts | Expands denylists (Cursor worktrees, Copilot auth/internal state, Antigravity OAuth/state exclusions). |
| src/core/manifest/schema.ts | Extends manifest tool-id schema to include antigravity. |
| src/core/config/schema.ts | Extends config tool-id schema to include antigravity. |
| src/commands/status.ts | Wires Antigravity adapter into status command capture map. |
| src/commands/setup.ts | Updates setup description to include Antigravity. |
| src/commands/secrets.ts | Adds Antigravity label handling in secrets command output. |
| src/commands/restore.ts | Wires Antigravity restore planning (multi-root) and adds re-auth reminder text. |
| src/commands/init.ts | Adds Antigravity display name and CLI parsing support for the new ToolId. |
| src/commands/backup.ts | Wires Antigravity into backup capture selection and labeling. |
| src/adapters/shared/configDir.ts | Replaces NUL-byte binary heuristic with decodeForCapture() + binary fail-safe scan. |
| src/adapters/registry.ts | Registers the Antigravity adapter in the adapter registry. |
| src/adapters/kilo/paths.ts | Captures Kilo tui.json(c) alongside main config for round-trip portability. |
| src/adapters/cursor/paths.ts | Widens Cursor frozen paths to include Cursor CLI config + global commands/rules. |
| src/adapters/cursor/index.ts | Uses decodeForCapture() + binary fail-safe scan in Cursor capture logic. |
| src/adapters/copilot/paths.ts | Switches Copilot frozen paths to user-authored config only (excluding config.json). |
| src/adapters/claude/capture.ts | Uses decodeForCapture() + binary fail-safe scan in Claude capture logic. |
| src/adapters/antigravity/paths.ts | Defines Antigravity’s three-root path model (home/User/.gemini) and allowlisted frozen paths. |
| src/adapters/antigravity/index.ts | Implements Antigravity adapter (capture/restore/detect/install planActions). |
| README.md | Updates supported tools and behavior descriptions (Cursor CLI + Antigravity + secret safety). |
| package.json | Bumps version to 0.2.0 and updates package description/keywords for expanded tool support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
src/core/config/schema.ts (1)
16-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider deriving the Zod enum from
TOOL_IDSto prevent drift.
toolIdSchemaduplicates the tool-id literal list already defined inTOOL_IDS(src/types.ts). Nothing enforces these stay in sync — a future addition to one and not the other would only surface as a runtime validation mismatch, not a type error.♻️ Possible refactor (derive from TOOL_IDS)
-const toolIdSchema = z.enum([ - "claude", - "codex", - "cursor", - "opencode", - "copilot", - "kilo", - "antigravity", -]); +// TOOL_IDS is `readonly ToolId[]`; the cast is safe because TOOL_IDS is guaranteed +// non-empty and exhaustive over ToolId. +const toolIdSchema = z.enum(TOOL_IDS as unknown as [ToolId, ...ToolId[]]);Same duplication exists in
src/core/manifest/schema.ts.🤖 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 `@src/core/config/schema.ts` around lines 16 - 24, The tool-id Zod enum is duplicated in toolIdSchema instead of being derived from TOOL_IDS, which can drift from the shared source of truth. Update toolIdSchema in the schema definitions to build from TOOL_IDS from src/types.ts so the validation list stays aligned with the typed constants, and apply the same refactor to the matching schema in src/core/manifest/schema.ts.src/adapters/antigravity/index.ts (3)
410-454: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
installCli'srunInstall(cmd)branch appears to be dead code.The docstring states installCommandFor returns null on every OS for antigravity, so the
await runInstall(cmd)path (Line 453) is currently unreachable, and it's also not wrapped in try/catch — inconsistent with the "never throws" guarantee stated for this function if a future OS mapping ever returns a non-null command.🤖 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 `@src/adapters/antigravity/index.ts` around lines 410 - 454, The installCli function contains an unreachable runInstall(cmd) branch because installCommandFor("antigravity", os) is documented to return null on every OS, so simplify the control flow around installCli and its cmd check to remove the dead path. If you keep the fallback for future OS mappings, make the runInstall(cmd) call explicitly guarded and wrapped so installCli still never throws, and keep the behavior aligned with the existing detect, isCliInstalled, and installCommandFor symbols.
254-297: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
capture()'s root-presence check doesn't mirrordetect()'s stricter Gemini rule.
detect()explicitly documents that Plain ~/.gemini alone is NOT enough — that can be a Gemini-CLI-only install (see its own comment at Line 423). Butcapture()'spresentfilter (Line 264-266) treats the"gemini"root as present whenever~/.geminiexists as a directory, regardless of whetherhome/userare present or whether theantigravity/subtree exists inside it. If a user has only ever run the legacy/standalone Gemini CLI (no Antigravity),~/.geminiwill exist, andcapture()will still walkGEMINI_FROZEN_PATHSfrom it — mislabeling that user's unrelated Gemini CLI config (GEMINI.md,settings.json) as Antigravity backup data underantigravity/gemini/*in the repo.This isn't a secret leak (same allowlist/denylist still applies) but is a real correctness/labeling gap relative to the documented detection intent.
♻️ Align gemini-root presence with detect()'s stricter check
const roots = captureRoots(ctx); const present: Array<{ root: Root; abs: string; frozen: readonly string[] }> = []; for (const r of roots) { if ((await ctx.fs.statKind(r.abs)) === "dir") present.push(r); } + + // A bare ~/.gemini (Gemini-CLI-only install) alone should not count as + // "Antigravity present" — mirror detect()'s stricter check. + const hasAntigravitySignal = + present.some((r) => r.root === "home" || r.root === "user") || + (await ctx.fs.statKind(path.join(geminiDir(ctx.toolHome), "antigravity"))) === "dir"; + const finalPresent = present.filter((r) => r.root !== "gemini" || hasAntigravitySignal);(and use
finalPresentin place ofpresentbelow)🤖 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 `@src/adapters/antigravity/index.ts` around lines 254 - 297, Align capture() with detect() by tightening the gemini root presence check so plain ~/.gemini alone does not count as Antigravity data; reuse the stricter logic already documented in detect() when building the present roots in capture(). Update the root filtering in capture() to only include the gemini root when the Antigravity-specific subtree or equivalent supporting roots are present, and then use the resulting finalPresent list for the walkFrozen loop and the empty-result warning path.
126-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAvoid the direct
node:fscall inreadMode
FsServicehasstatKind, but no mode-aware stat API, so this helper has to bypassctx.fsto preserve executable bits. Add a mode-preserving helper there and use it here so fixture/virtual filesystems stay injectable and tests can cover this path.🤖 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 `@src/adapters/antigravity/index.ts` around lines 126 - 134, The readMode helper is bypassing ctx.fs by importing node:fs directly, which makes the filesystem path non-injectable and harder to test. Add a mode-preserving stat helper to FsService (alongside statKind) that can return the file mode while still working with fixture/virtual filesystems, then update readMode in Antigravity’s index flow to call that FsService helper instead of node:fs. Keep the executable-bit preservation behavior intact while routing all filesystem access through the injected service.
🤖 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 `@src/adapters/antigravity/paths.ts`:
- Around line 64-72: Update antigravityUserDir in paths.ts so it resolves the
User data directory under the Antigravity IDE application root instead of
Antigravity, since current installs place VS Code-style data there. Keep the
existing userHome/appUserConfigRoot logic, but change the final joined segment
in antigravityUserDir to match the Antigravity IDE install layout so
settings.json, keybindings.json, and snippets are found.
In `@src/adapters/kilo/paths.ts`:
- Around line 13-14: Fix the doc comment in the paths module by replacing the
nonsensical word “arbella” with the intended phrase “this adapter” in the
runtime-state boundary description. Update the comment near the path helper
symbols in src/adapters/kilo/paths.ts so it reads naturally and accurately
describes that this adapter never touches the ~/.kilocode/cli tree.
In `@src/utils/capture-bytes.ts`:
- Around line 77-84: The UTF-16 no-BOM heuristic in capture-bytes.ts is too
permissive and can misclassify tiny binary payloads as text. Update the logic
around the pairs/nulOdd/nulEven checks in the captureBytes flow to require a
minimum dominant-NUL count before either UTF-16 path is accepted, then keep the
existing DENSITY and RATIO checks as secondary gates. Use the existing
decodeUtf16be and utf16le branches, but add the new minimum-signal guard so
small inputs like single-byte or very short binaries do not decode as text.
In `@test/unit/configdir-adapters.test.ts`:
- Line 217: The test fixture in configdir-adapters.test.ts uses a contiguous
GitHub-token-shaped literal for COPILOT_TOKEN, which triggers secret scanners.
Update the test to construct the same value at runtime inside the relevant test
setup instead of storing it as one literal, and keep the existing constant name
and any assertions in the configdir-adapters.test.ts suite unchanged.
- Around line 346-350: The secret-leak assertion in the u16 capture test only
decodes binary payloads as UTF-8, so it can miss UTF-16LE secrets. Update the
check in the u16Capture.files loop to inspect binary captures using UTF-16
decoding as well as the existing text/base64 path, and keep the assertion in the
same test case that references U16_SECRET.
---
Nitpick comments:
In `@src/adapters/antigravity/index.ts`:
- Around line 410-454: The installCli function contains an unreachable
runInstall(cmd) branch because installCommandFor("antigravity", os) is
documented to return null on every OS, so simplify the control flow around
installCli and its cmd check to remove the dead path. If you keep the fallback
for future OS mappings, make the runInstall(cmd) call explicitly guarded and
wrapped so installCli still never throws, and keep the behavior aligned with the
existing detect, isCliInstalled, and installCommandFor symbols.
- Around line 254-297: Align capture() with detect() by tightening the gemini
root presence check so plain ~/.gemini alone does not count as Antigravity data;
reuse the stricter logic already documented in detect() when building the
present roots in capture(). Update the root filtering in capture() to only
include the gemini root when the Antigravity-specific subtree or equivalent
supporting roots are present, and then use the resulting finalPresent list for
the walkFrozen loop and the empty-result warning path.
- Around line 126-134: The readMode helper is bypassing ctx.fs by importing
node:fs directly, which makes the filesystem path non-injectable and harder to
test. Add a mode-preserving stat helper to FsService (alongside statKind) that
can return the file mode while still working with fixture/virtual filesystems,
then update readMode in Antigravity’s index flow to call that FsService helper
instead of node:fs. Keep the executable-bit preservation behavior intact while
routing all filesystem access through the injected service.
In `@src/core/config/schema.ts`:
- Around line 16-24: The tool-id Zod enum is duplicated in toolIdSchema instead
of being derived from TOOL_IDS, which can drift from the shared source of truth.
Update toolIdSchema in the schema definitions to build from TOOL_IDS from
src/types.ts so the validation list stays aligned with the typed constants, and
apply the same refactor to the matching schema in src/core/manifest/schema.ts.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 20acbb77-8118-4f15-83e3-65ae8ef81c92
📒 Files selected for processing (29)
README.mdpackage.jsonsrc/adapters/antigravity/index.tssrc/adapters/antigravity/paths.tssrc/adapters/claude/capture.tssrc/adapters/copilot/paths.tssrc/adapters/cursor/index.tssrc/adapters/cursor/paths.tssrc/adapters/kilo/paths.tssrc/adapters/registry.tssrc/adapters/shared/configDir.tssrc/commands/backup.tssrc/commands/init.tssrc/commands/restore.tssrc/commands/secrets.tssrc/commands/setup.tssrc/commands/status.tssrc/core/config/schema.tssrc/core/manifest/schema.tssrc/core/sanitizer/denylist.tssrc/core/secrets/index.tssrc/platform/install.tssrc/platform/os.tssrc/types.tssrc/utils/capture-bytes.tstest/unit/antigravity-adapter.test.tstest/unit/configdir-adapters.test.tstest/unit/cursor-cli-and-antigravity-wiring.test.tstest/unit/new-tool-platform.test.ts
Codex (both P2, real):
- backup: toolRepoDataRoots() now owns antigravity/user + antigravity/gemini, so
replaceToolFiles() mirrors ALL of Antigravity's roots (locally deleted files
can no longer resurrect from the repo) and the generated .gitignore scopes its
rules over them too.
- restore: safetySourcesForTool() snapshots Antigravity's restore targets before
a repo-wins pull — the User dir (both name variants) and, surgically, only the
~/.gemini frozen paths (the dir is shared with the Gemini CLI and holds a
browser profile + OAuth files that must not be bulk-copied).
Copilot:
- capture-bytes: valid UTF-16 is always even-length; odd-length buffers (a lone
0x00, a truncated BOM file) no longer take a lossy UTF-16 decode and are stored
byte-exact as binary. Follow-up hardening from the same test round: NUL-free
content is stored as text only when strictly valid UTF-8 (lenient decode
mangled Latin-1 to U+FFFD on round-trip).
CodeRabbit:
- antigravity: handle the post-2.0 dir split ("Antigravity IDE" +
~/.antigravity-ide vs classic "Antigravity" + ~/.antigravity; Google AI forum,
May 2026 — verified live: macOS 2.0.6 still uses the classic names). Capture,
restore, and detect now probe both candidates and prefer the existing one
(IDE-variant first), with the classic name as the fresh-machine default.
- capture-bytes: require >= 2 dominant-parity NULs before the no-BOM UTF-16
heuristic so tiny blobs like [0x00, 0x01] stay byte-exact binary.
- tests: build token-shaped fixtures at runtime (join) so secret scanners do not
flag them; leak assertions on base64 payloads now check the UTF-16LE decoding
too, not just UTF-8.
- kilo doc "which arbella never touches": kept — "arbella" is the product name,
the sentence is intentional (skipped with reason on the thread).
Tests: +3 (219 total), including a behavioral capture+restore round-trip against
the "Antigravity IDE" layout and byte-classifier regressions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACwssULEcxKCbB28zZRZ63
…SON pass
Found by a live end-to-end test (real `arbella init`/`push`/`pull` against a
sandboxed HOME with planted fake secrets across all 7 tools): an opencode MCP
env var named just `KEY` — no api/token/secret stem, opaque value — reached the
repo unredacted. Codex's TOML pass already treats env/headers tables as secret
containers (every leaf below is redacted, whatever its name); the JSON pass had
no such rule, so JSON-config tools (opencode, cursor, antigravity, copilot,
claude, kilo) relied on key-name matching alone — which arbitrary env-var names
defeat.
- patterns.ts: shared SECRET_CONTAINER_KEYS ("env", "environment", "headers") +
isSecretContainerKey(), and *_key/-key suffixes in isSecretKey so vendor names
like OPENAI_KEY / DEEPL-KEY are caught anywhere. Deliberately NOT bare "key":
VS Code-style keybindings.json entries are {"key": "cmd+k"} and must survive.
- sanitizer/index.ts: cloneRedact threads an inSecretContainer flag — once the
walk descends under an env/environment/headers map, EVERY leaf in that subtree
is redacted, mirroring the TOML pass.
E2E verification (real binary, file:// remote, fresh restore home): 9/9 planted
secrets now excluded or redacted (was 8/9); {{TOOL_HOME}}/{{HOME}} templating,
per-OS multi-root restore placement, safety-backup, and reauth reminders all
verified live. +5 unit tests (224 total), including keybindings survival and
dotted-settings-key non-container regression guards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACwssULEcxKCbB28zZRZ63
The binary-branch fail-safe scanned only the lossy UTF-8 view, so a credential embedded as NUL-interleaved UTF-16 bytes inside a binary-classified file (s\0k\0-\0...) was invisible and the file would be base64-stored raw. This matters most for the odd-length route the previous fix added: a truncated UTF-16 config lands in the binary branch while still carrying a full secret. - capture-bytes: new binaryScanViews() — the UTF-8 view plus UTF-16LE and UTF-16BE views at BOTH byte alignments (a leading header byte shifts the interleave). Views are for scanning only; stored payloads stay byte-exact. - All four capture sites (shared/configDir, cursor, antigravity, claude) loop the views: any hit drops the file with a warning + SecretRef instead of storing it. Tests: +6 (230 total) — view-visibility units (LE even/odd offset, BE, UTF-8) and a behavioral opencode capture where a balanced-NUL-parity blob with an embedded UTF-16LE token is skipped while an equal-shape benign blob is still captured as base64. Verified live end-to-end: a planted UTF-16-in-binary secret is skipped on `arbella push` and a UTF-8+UTF-16 deep scan of the pushed repo finds zero leaks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACwssULEcxKCbB28zZRZ63
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
src/adapters/antigravity/paths.ts (1)
84-87: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor duplication between default and candidate list.
antigravityUserDir's classic default"Antigravity"duplicatesANTIGRAVITY_APP_DIR_NAMES[1]. Deriving it from the constant (e.g.ANTIGRAVITY_APP_DIR_NAMES[ANTIGRAVITY_APP_DIR_NAMES.length - 1]) would prevent silent drift if the list is ever reordered/edited.🤖 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 `@src/adapters/antigravity/paths.ts` around lines 84 - 87, The antigravityUserDir helper currently hardcodes the classic app directory name, which duplicates ANTIGRAVITY_APP_DIR_NAMES[1] and can drift if the list changes. Update antigravityUserDir in paths.ts to derive the default directory segment from ANTIGRAVITY_APP_DIR_NAMES instead of repeating the string, using the existing constant near the function so the fallback stays aligned with the candidate list.src/utils/capture-bytes.ts (1)
32-49: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUnbounded full-buffer scan for every binary file.
decodeForCapture's own classification sniff is capped atSNIFF_LIMITfor performance, butbinaryScanViewsconverts the entire buffer into 5 separate lossy string views, with no size bound. This runs on every file classified as binary (images, sqlite DBs, other blobs per the module's own doc comment) unlessincludeSecretsis set, so large binary artifacts pay a 5x full-buffer conversion cost on every capture.Consider capping the scan to a reasonable prefix/sample size (or skipping the scan and simply excluding oversized binaries) to avoid unbounded CPU/memory use on large files.
🤖 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 `@src/utils/capture-bytes.ts` around lines 32 - 49, The binary secret-scan path in binaryScanViews currently converts the entire Buffer into multiple lossy string views, which creates unbounded work for large binary captures. Update binaryScanViews (and any caller like decodeForCapture that relies on it) to scan only a bounded prefix or sample size consistent with SNIFF_LIMIT, or skip secret scanning for oversized binaries, so binary files do not incur full-buffer 5x conversion cost.src/core/sanitizer/patterns.ts (1)
246-250: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win"_key"/"-key" suffix risks false positives on legitimate non-secret fields.
Fields like
public_key,primary_key,partition_key,sort_key, orforeign_keyare common in SSH/GPG/DB/ORM configs and are not secrets (a public key is, by definition, shareable), but they'll now match this suffix and get their values redacted.Consider excluding a short list of known non-secret "_key" terms (e.g.
public_key,primary_key,partition_key,sort_key,foreign_key) to reduce unnecessary over-redaction of legitimate config values.🤖 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 `@src/core/sanitizer/patterns.ts` around lines 246 - 250, The "_key"/"-key" suffixes in the sanitizer pattern list are over-matching legitimate non-secret fields and causing unnecessary redaction. Update the matching logic in patterns.ts around the key suffix entries to exclude known safe identifiers such as public_key, primary_key, partition_key, sort_key, and foreign_key, so the sanitizer still catches vendor-style secret names without redacting common database/crypto config fields. Keep the change localized to the pattern definitions used by the sanitizer so existing secret-detection behavior remains intact.test/unit/sanitizer.test.ts (1)
293-304: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winTest doesn't exercise the scenario its comment describes.
The comment describes guarding against
"terminal.integrated.env.osx"(a dotted key containing "env") being misread as a secret container, but the actual payload (editor.fontSize,workbench.colorTheme) has no such key and no nested object at all. As written, this test would pass even if dotted-key container matching were broken — it isn't a regression guard for the described case.♻️ Proposed fix to actually exercise the dotted-key scenario
it("does not misread dotted VS Code settings keys as containers", () => { // "terminal.integrated.env.osx" is ONE key (not an env container); its // object value is a real env map though — but only once a key literally // named env/environment/headers introduces it. const settings = JSON.stringify( - { "editor.fontSize": 14, "workbench.colorTheme": "Dark" }, + { "terminal.integrated.env.osx": { MY_VAR: "not-a-secret-value" } }, null, 2, ); const res = sanitizer.sanitizeFile(settings, "cursor", "settings.json"); expect(res.changed).toBe(false); });🤖 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 `@test/unit/sanitizer.test.ts` around lines 293 - 304, The sanitizer test is not covering the dotted-key regression it describes, so update the payload in the settings.json case to include a literal dotted key like sanitizer.sanitizeFile with "terminal.integrated.env.osx" and a non-secret object value that should not be treated as an env container. Keep the assertion on res.changed and verify the test still uses the existing sanitizeFile path so it specifically guards against misreading dotted VS Code settings keys as containers.
🤖 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 `@test/unit/configdir-adapters.test.ts`:
- Line 369: The BIN_SECRET fixture in configdir-adapters.test.ts is a contiguous
secret-shaped literal that can trigger scanners. Update the BIN_SECRET constant
to be assembled at runtime in the same style as the other fixed fixtures in this
file, so the test still uses the same value but no longer contains a raw
Anthropic-like token prefix.
---
Nitpick comments:
In `@src/adapters/antigravity/paths.ts`:
- Around line 84-87: The antigravityUserDir helper currently hardcodes the
classic app directory name, which duplicates ANTIGRAVITY_APP_DIR_NAMES[1] and
can drift if the list changes. Update antigravityUserDir in paths.ts to derive
the default directory segment from ANTIGRAVITY_APP_DIR_NAMES instead of
repeating the string, using the existing constant near the function so the
fallback stays aligned with the candidate list.
In `@src/core/sanitizer/patterns.ts`:
- Around line 246-250: The "_key"/"-key" suffixes in the sanitizer pattern list
are over-matching legitimate non-secret fields and causing unnecessary
redaction. Update the matching logic in patterns.ts around the key suffix
entries to exclude known safe identifiers such as public_key, primary_key,
partition_key, sort_key, and foreign_key, so the sanitizer still catches
vendor-style secret names without redacting common database/crypto config
fields. Keep the change localized to the pattern definitions used by the
sanitizer so existing secret-detection behavior remains intact.
In `@src/utils/capture-bytes.ts`:
- Around line 32-49: The binary secret-scan path in binaryScanViews currently
converts the entire Buffer into multiple lossy string views, which creates
unbounded work for large binary captures. Update binaryScanViews (and any caller
like decodeForCapture that relies on it) to scan only a bounded prefix or sample
size consistent with SNIFF_LIMIT, or skip secret scanning for oversized
binaries, so binary files do not incur full-buffer 5x conversion cost.
In `@test/unit/sanitizer.test.ts`:
- Around line 293-304: The sanitizer test is not covering the dotted-key
regression it describes, so update the payload in the settings.json case to
include a literal dotted key like sanitizer.sanitizeFile with
"terminal.integrated.env.osx" and a non-secret object value that should not be
treated as an env container. Keep the assertion on res.changed and verify the
test still uses the existing sanitizeFile path so it specifically guards against
misreading dotted VS Code settings keys as containers.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 961d9793-188c-41f5-a4a4-00098aa33c4a
📒 Files selected for processing (15)
src/adapters/antigravity/index.tssrc/adapters/antigravity/paths.tssrc/adapters/claude/capture.tssrc/adapters/cursor/index.tssrc/adapters/shared/configDir.tssrc/commands/backup.tssrc/commands/restore.tssrc/core/sanitizer/index.tssrc/core/sanitizer/patterns.tssrc/utils/capture-bytes.tstest/unit/antigravity-adapter.test.tstest/unit/capture-bytes.test.tstest/unit/configdir-adapters.test.tstest/unit/cursor-cli-and-antigravity-wiring.test.tstest/unit/sanitizer.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- src/adapters/shared/configDir.ts
- src/adapters/claude/capture.ts
- src/adapters/antigravity/index.ts
- test/unit/antigravity-adapter.test.ts
- src/adapters/cursor/index.ts
Summary
Prepares the v0.2.0 release. This branch lands on top of the two merged PRs (#5 opencode/Copilot/Kilo adapters, #6 community-health files) and adds two more tools, fixes the cross-platform bugs the PR #5 review surfaced, and hardens secret handling.
New tool support
settings.json,keybindings.json,snippets/) → per-OS (~/Library/Application Support·~/.config·%APPDATA%)~/.antigravity/extensions/extensions.json(extension list)~/.geminiagent config (GEMINI.md,settings.json,antigravity/mcp_config.json,skills/)~/.gemini(oauth_creds.json,google_accounts.json,*.pb,history/). A test asserts the OAuth token reaches no captured file.cursor-agent) — it shares~/.cursorwith the IDE, so rather than a duplicate adapter (which would double-capturemcp.json/skills), the existing Cursor adapter is widened to also capturecli-config.json+ globalcommands/+rules/, honorCURSOR_CONFIG_DIR, and exclude runtimeworktrees/.Cross-platform fixes (verified against official docs)
~/.config/<tool>(XDG on every OS) instead of%APPDATA%\<tool>. The old mapping madedetect()report the tool absent andrestorewrite where the CLI never reads — the exact mac→Windows breakage this repo is meant to prevent.settings.json,mcp-config.json,lsp-config.json,copilot-instructions.md,instructions/,agents/,hooks/) and hard-denyconfig.json, which per GitHub's config-dir reference is auto-managed auth/plugin state — restoring a stale one would clobber the target's sign-in.tui.json(c)(terminal-UI settings) so they round-trip.Security hardening (from a review of the secret-handling paths)
looksBinaryheuristic base64'd any file containing a NUL byte straight into the repo, skipping the sanitizer entirely — even withincludeSecrets=false. A UTF-16 config (PowerShell's defaultOut-File/Set-Contentencoding on Windows) trips this, so a secret-bearingmcp_config.json/settings.jsoncould have shipped unredacted. NewdecodeForCapture()decodes UTF-16 to text so it's sanitized across all four capture sites (antigravity, opencode/Copilot/Kilo, cursor, claude), with a fail-safe that drops any "binary" file containing secret-shaped bytes. Regression-tested with a UTF-16LE config.~/.geminiOAuth files (oauth_creds.json,google_accounts.json,*.pb) are excluded by the allowlist (never opened) and the denylist; the review confirmed no directory-walk or symlink path can reach them, and a test asserts the OAuth token reaches no captured file.Test plan
npm run typecheck— cleannpm run test— 202 passed (22 files; +27 new)npm run build+npm pack --dry-run— clean (v0.2.0, 5 files)npm audit --omit=dev— 0 vulnerabilities (shipped deps)--help,setup/status/push/pull/auth/secrets --help,--versionwin32); per-OS Antigravity User-dir; UTF-16 config sanitized-not-shipped-rawpush/pullon a real machine per OS (recommended before tagging the release)🤖 Generated with Claude Code
Summary by CodeRabbit