Skip to content

feat: add vibe copy subcommand with Claude Code worktree hook#359

Merged
kexi merged 12 commits intodevelopfrom
feat/cow-copy-skill
Feb 22, 2026
Merged

feat: add vibe copy subcommand with Claude Code worktree hook#359
kexi merged 12 commits intodevelopfrom
feat/cow-copy-skill

Conversation

@kexi
Copy link
Owner

@kexi kexi commented Feb 21, 2026

Summary

  • Extract shared copy logic (copyFiles, copyDirectories, withConcurrencyLimit) from start.ts into utils/copy-runner.ts for reuse
  • Add vibe copy subcommand that copies files/directories from main worktree to secondary worktree using CoW based on .vibe.toml [copy] configuration
  • Add Claude Code PostToolUse(EnterWorktree) hook in .claude/settings.json for automatic copying when claude --worktree creates a worktree
  • vibe copy reads target path from stdin JSON (.cwd field), enabling a simple hook command without jq or bash -c wrappers

Test plan

  • pnpm run check:all passes (362 tests, lint, typecheck, docs)
  • Run vibe copy --dry-run in a worktree → shows copy targets
  • Run vibe copy in a worktree → CoW copy executes
  • Run vibe copy in main worktree → error message
  • Run claude --worktree test → hook auto-copies

🤖 Generated with Claude Code

kexi and others added 3 commits February 21, 2026 20:27
Extract copyFiles, copyDirectories, and withConcurrencyLimit from
start.ts into a shared module (utils/copy-runner.ts) so the new
vibe copy command can reuse the same CoW copy infrastructure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `vibe copy` command that copies files/directories from the main
worktree to a secondary worktree using CoW, based on .vibe.toml [copy]
configuration.

Target resolution: --target flag > stdin JSON .cwd > git repo root.
The stdin JSON support allows a simple Claude Code PostToolUse hook
(`vibe copy --quiet`) without requiring jq or bash -c wrappers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add English and Japanese documentation for the vibe copy command,
including Claude Code hook setup guide. Update command index pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 21, 2026

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook

Overall this is a well-structured addition. The refactoring of shared copy logic into copy-runner.ts is clean, docs are bilingual (good), and tests cover the core happy/sad paths. A few issues worth addressing before merge:


Bugs / Correctness

1. Misleading test case (copy.test.ts:255)

The test "exits with error when run on main worktree with --target pointing to same path" is not testing what its name says. It configures isMainWorktree: true so that cwd itself is the main worktree, rather than a secondary worktree whose --target happens to equal the main path. The interesting case — running from a secondary worktree with --target /main-path — is not tested.

// Should also add: cwd is secondary, but --target == main
createCopyTestContext({ cwd: "/tmp/worktree", mainWorktreePath: "/tmp/main-repo", isMainWorktree: false })
await copyCommand({ target: "/tmp/main-repo" }, ctx); // should fail

2. config! non-null assertions (copy.ts:422, 431)

After the hasCopyConfig guard, TypeScript cannot narrow config to non-undefined because hasCopyConfig is a local boolean rather than a type guard — so the ! assertions are needed but smell. Restructuring the early-return guard eliminates the need:

if (!config?.copy?.files?.length && !config?.copy?.dirs?.length) {
  verboseLog("No copy configuration found. Skipping.", outputOpts);
  return;
}
// config is non-undefined here; no ! needed below

Architecture / Design

3. copy.ts imports resolveCopyConcurrency from start.ts

This introduces a copy -> start dependency that did not exist before. resolveCopyConcurrency is now exported from start.ts solely to serve copy.ts. Moving it into copy-runner.ts — where the copy functions already live, and which both commands already depend on — would be more cohesive.


Security

4. Unvalidated stdin-provided path used for file operations (copy.ts:380)

const targetPath = target ?? stdinTarget ?? (await getRepoRoot(ctx));

stdinTarget comes from untrusted JSON on stdin and flows directly into copyFiles/copyDirectories as the destination path. Per the project's security checklist, paths should pass through validatePath(). A crafted hook payload like {"cwd": "../../sensitive"} could direct copies to unintended locations.

5. Hook command uses shell operators (settings.json:15)

"command": "vibe copy --quiet 2>/dev/null || true"

The security checklist recommends spawn over shell string execution. While this is Claude Code hook config rather than vibe's own process spawning, it ships in the repo and establishes a shell-string pattern. Worth at least documenting the trade-off. The 2>/dev/null also silently discards all stderr, hiding meaningful errors even in non-quiet mode.


Missing Test Coverage

6. --dry-run is untested

No test verifies that --dry-run suppresses actual file operations and prints dry-run output. It is a prominently documented flag and deserves at least one test.

7. Stdin-based target resolution is untested

readTargetFromStdin — the core of hook mode — has zero unit tests. Tests should cover valid JSON, invalid JSON, empty stdin, and missing .cwd field.


Documentation

8. ASCII art diagram should be Mermaid (commands/copy.mdx, ja/commands/copy.mdx)

Per .claude/rules/docs-i18n.md, Mermaid is preferred over ASCII art. The "How It Works" flow diagram in both language versions should use a Mermaid flowchart block.

9. README.md / README.ja.md not updated

If the top-level READMEs list commands, vibe copy should appear there too.


Minor Nits

  • --target in the global arg parser (main.ts:39): silently accepted by every subcommand with no effect. A user who types vibe start --target ... gets no error. Consider noting it as copy-only in the help text.
  • Recursive executeNext in withConcurrencyLimit: will stack-overflow on very large item lists. Fine for typical file lists, but a comment noting the assumption helps future readers.
  • No stdin size cap in readTargetFromStdin: a guard (e.g. 1 MB) would prevent unbounded memory use from a misbehaving stdin.

Summary

Area Status
Core refactoring (copy-runner.ts) ✅ Clean
vibe copy command structure ✅ Good
Bilingual docs (en + ja)
i18n index updates
Test coverage ⚠️ Gaps (dry-run, stdin, target-to-main)
Security (stdin path validation) ⚠️ Needs validatePath()
Architecture (start.ts coupling) ⚠️ Minor — move to copy-runner.ts
Docs diagram style ⚠️ Use Mermaid per project rules
README update ❓ Check if needed

The implementation is solid. The main items to address are path validation for stdin-provided targets and the start.ts coupling. Closing the test gaps and converting the diagram to Mermaid would also be good before merge.

🤖 Generated with Claude Code

- Move resolveCopyConcurrency to copy-runner.ts for better cohesion
- Add stdin path validation (isAbsolute, validatePath, MAX_STDIN_SIZE)
- Remove non-null assertions on config with optional chaining
- Remove 2>/dev/null from hook command for better debuggability
- Convert ASCII art to Mermaid diagrams in docs
- Expand test coverage with stdin resolution and dry-run tests
- Add vibe copy to README command tables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 21, 2026

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Development Commands

# Run ALL checks for ALL packages (REQUIRED before creating PR)
pnpm run check:all

# Run checks for core package only
pnpm run check:core

# Run checks for docs package only
pnpm run check:docs

# Run checks for video package only
pnpm run check:video

# Run in development mode
pnpm run dev <command>

# Run tests
pnpm run test

# Run a single test file
pnpm run test packages/core/src/path/to/test.ts

# Enable vibe shell function
source .vibedev

Architecture

  • Runtime: Bun-based CLI tool
  • Purpose: Git worktree management (start, clean, trust, untrust, verify, config, upgrade)
  • CoW Optimization: Copy-on-Write support for APFS, Btrfs, XFS filesystems

Source Structure

packages/core/src/
├── commands/   # CLI command implementations
├── context/    # Context management
├── services/   # Business logic services
├── utils/      # Utility functions
├── runtime/    # Runtime abstraction layer
├── native/     # Native platform integrations
├── types/      # TypeScript type definitions
└── errors/     # Error handling

Branch Strategy

  • main: Release versions only
  • develop: Main development branch
  • Topic branches should be created from develop
  • IMPORTANT: Never push directly to main or develop branches
  • PRs must always target the develop branch

PR/Commit Guidelines

  • IMPORTANT: Before creating a PR, always run pnpm run check:all and ensure all checks pass
  • Title format: <type>: <description>
    • Types: feat, fix, docs, refactor, test, chore
  • Write in English
  • Follow GNU Coding Standards

Security

  • SHA-256 based configuration trust mechanism
  • Use Node.js spawn (avoid shell string execution)
  • Path validation with validatePath()
  • Shell output escaping with escapeShellPath() for all cd output
  • ESLint security plugin (eslint-plugin-security) and custom vibe-security rules for static analysis
  • See docs/SECURITY_CHECKLIST.md for the full 13-category CLI security checklist

@claude
Copy link

claude bot commented Feb 21, 2026

test message please ignore

@claude
Copy link

claude bot commented Feb 21, 2026

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook

Overall this is a well-structured addition. The refactoring of shared copy logic into copy-runner.ts is clean, and the stdin-based hook integration is a smart design. Here are my observations:


Issues

1. process.env used directly instead of runtime abstraction (copy-runner.ts)

if (process.env.VIBE_DEBUG) {
  console.error(`[vibe] Copy strategy: ${dirStrategy.name}`);
}

This bypasses the runtime abstraction layer used everywhere else in the codebase. Should be:

if (ctx.runtime.env.get("VIBE_DEBUG")) {
  // ...
}

The ctx parameter is already available in copyDirectories.

2. console.warn used directly instead of warnLog (copy-runner.ts)

console.warn(`Warning: Failed to copy ${file}: ${errorMessage}`);

This is inconsistent with the rest of the codebase that uses warnLog from ./output.ts. The function already imports warnLog.

3. Path normalization for main worktree detection (copy.ts)

const isMain = originPath === targetPath;

String equality on paths is fragile — trailing slashes, symlinks, or inconsistent resolution could cause this check to fail silently. Both values come from git commands, so it is likely fine in practice, but using path.resolve() on both sides would be more defensive:

import { resolve } from "node:path";
const isMain = resolve(originPath) === resolve(targetPath);

4. Inconsistency between .claude/settings.json and documentation

The committed .claude/settings.json has:

"command": "vibe copy --quiet || true"

While the docs (commands/copy.mdx) show:

"command": "vibe copy --quiet 2>/dev/null || true"

The doc version is better since it suppresses error output that --quiet does not suppress. Consider aligning the two — either add 2>/dev/null to the committed settings or update the docs to match.


Minor Observations

withConcurrencyLimit is now exported — previously private in start.ts. Tests for it in copy-runner.test.ts are good. The start.test.ts import update is correct.

Test coveragecopy.test.ts covers all main error paths and stdin resolution scenarios well. One gap: there is no test that verifies the actual file copy does occur when config is present and dryRun: false. The dry-run test checks that copyFile is NOT called, but the inverse is not verified. Minor, since the dry-run test indirectly validates the code path.

copyDirectories has a default ctx parameter (= getGlobalContext()). This is inconsistent with the convention of requiring explicit context passing, but since the function is exported and the default is just a convenience fallback, it is acceptable.

--target option is registered globally in parseArgsOptions — consistent with how other command-specific options like --base, --track are handled. Fine as-is.

Security — the readTargetFromStdin implementation looks solid: terminal detection, 1MB size cap, absolute path check, and validatePath() validation all applied before using the value. Good defense-in-depth.


Summary

Category Status
Code extraction/refactoring Clean
Test coverage Good (minor gap noted)
Docs (EN + JA) Both updated and in sync
README (EN + JA) Both updated
Security (stdin) Well-handled
Runtime abstraction consistency process.env / console.warn bypass needs fix
Path comparison robustness Minor concern
Settings/docs consistency Minor inconsistency

The process.env / console.warn issues in copy-runner.ts are the most worth fixing before merge since they break the runtime abstraction pattern that enables testability.

- Use path.resolve() for worktree path comparison to handle edge cases
- Replace process.env with ctx.runtime.env.get() for runtime abstraction
- Replace console.warn with warnLog for consistent output handling
- Restore 2>/dev/null in hook command to match documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 21, 2026

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook

Overall this is a well-structured PR. The refactoring cleanly extracts shared copy logic, the new command is well-designed for hook usage, and the test/doc coverage is thorough. A few items worth addressing:


Issues

1. --quiet does not suppress individual copy failure warnings

copyFiles and copyDirectories in copy-runner.ts call warnLog(...) without OutputOptions, so --quiet has no effect on failure warnings. Either accept outputOpts as a parameter and thread it through, or document that copy failure warnings are always shown regardless of --quiet. The latter is also a reasonable design choice (errors should be visible), but it should be explicit.

2. Dead exitCodes option in createCopyTestContext

The exitCodes?: number[] field is declared in the options interface (copy.test.ts:144) but destructured and never used in the mock implementation. This dead code will confuse future test authors who assume it has an effect. Remove it or wire it up.


Minor

3. ProgressTracker created unconditionally in dry-run mode

In copy.ts, the tracker is instantiated and then guarded by const shouldStartTracker = !dryRun. If ProgressTracker construction has any side effects (e.g. writing to terminal), those would fire even in dry-run mode. Worth confirming new ProgressTracker(...) is side-effect-free at construction time.

4. copyDirectories has a default ctx = getGlobalContext()

All existing callers pass ctx explicitly, so the default parameter is never exercised in production. Making it a required parameter (like copyFiles) would be cleaner and would eliminate the implicit global context coupling.

5. Hook command silences stderr entirely

"command": "vibe copy --quiet 2>/dev/null || true"

With both --quiet and 2>/dev/null, all diagnostic output is lost. This is intentional for non-disruptive hook behavior, but means genuine failures (e.g. filesystem error, binary not found) are completely silent. Consider whether just --quiet without redirecting stderr might be a better middle ground, so critical errors are still surfaced in Claude Code's hook output.


Strengths

  • The extraction of copyFiles, copyDirectories, and withConcurrencyLimit into copy-runner.ts is clean; the interface change from VibeConfig to string[] (patterns) is an improvement.
  • The stdin target resolution is properly guarded: terminal check, 1 MB size cap, absolute path assertion, and validatePath() call.
  • Test coverage is comprehensive — the stdin resolution edge cases (invalid JSON, missing .cwd field, terminal stdin, --target taking precedence) are all tested.
  • Both README.md/README.ja.md and the full English/Japanese docs are updated in sync.
  • No GPL-licensed dependencies introduced.
  • VIBE_DEBUG check now uses ctx.runtime.env.get() instead of process.env directly — consistent with the runtime abstraction.

@kexi kexi self-assigned this Feb 21, 2026
- Change console.error to console.warn for debug output consistency
- Remove dead code exitCodes field from test helper
- Make ctx parameter required in copyDirectories to avoid implicit global context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 21, 2026

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook

Overall this is a well-structured PR. The extraction of shared copy logic into copy-runner.ts is clean, security concerns on stdin are properly handled, and both language docs are kept in sync. A few issues and suggestions below.


Issues

1. Orphaned JSDoc comment in copy.test.ts (lines 112–118)

There are two consecutive JSDoc blocks where the first is orphaned — it describes createCopyTestContext but appears before buildTrustedSettingsJson:

/**
 * Helper to create a mock context for copy command tests.
 * Simulates git worktree list with a main worktree and an optional secondary worktree.
 */
/**
 * Build a settings.json content that trusts .vibe.toml with skipHashCheck enabled.

The first comment should be moved to sit directly above createCopyTestContext.


2. withConcurrencyLimit order test may be fragile (copy-runner.test.ts line 953–959)

it("executes all items", async () => {
  const results: number[] = [];
  await withConcurrencyLimit([1, 2, 3, 4, 5], 3, async (item) => {
    results.push(item);
  });
  expect(results).toEqual([1, 2, 3, 4, 5]);
});

With limit=3, three concurrent chains start simultaneously. Even though this handler has no await, Promise microtask scheduling is not guaranteed to produce sequential order. The test passes today due to V8's microtask ordering, but could break under different JS engines or if the handler gains any await. Consider using toEqual(expect.arrayContaining([1, 2, 3, 4, 5])) and checking the length separately, or sort the results before asserting.


3. Missing test: actual file copy is invoked in non-dry-run mode

copy.test.ts verifies that copyFile is not called in dry-run mode, but there is no corresponding test asserting that copyFile is called when a valid [copy] config exists and dryRun is false. Adding this would complete the test symmetry.


Suggestions

4. resolve() vs realpath() for main worktree detection (copy.ts line 680)

const isMain = resolve(originPath) === resolve(targetPath);

path.resolve() normalises . and .. segments but does not follow symlinks. On macOS, /tmp/private/tmp, so paths derived from the hook's stdin JSON and from git rev-parse could differ if one goes through the symlink. realpath() (available via ctx.runtime.fs.realPath) would be more reliable. This is an edge case, but it's the kind of subtle bug that only appears in CI on certain platforms.


5. consoleWarnSpy in test context is never explicitly restored

const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

The spy is created but not returned or used in any assertion, and each test manually calls consoleErrorSpy.mockRestore() while leaving consoleWarnSpy to be cleaned up by afterEach(() => vi.restoreAllMocks()). This works, but either: (a) remove the manual consoleErrorSpy.mockRestore() calls in each test (since afterEach handles it), or (b) include consoleWarnSpy in the returned object if you want assertions on it. The current mixed approach is inconsistent.


6. Debug log now uses console.warn instead of console.error (copy-runner.ts line 1148)

console.warn(`[vibe] Copy strategy: ${dirStrategy.name}`);

The old code used console.error. Using console.warn for a [vibe] debug message is a minor but reasonable improvement — just noting it as an intentional change in case it affects any downstream log filters.


Positives

  • Security on stdin is solid: size cap (MAX_STDIN_SIZE = 1 MB), absolute path check, validatePath() call, and a catch-all try/catch returning undefined on any failure.
  • ctx.runtime.env.get("VIBE_DEBUG") replaces process.env.VIBE_DEBUG — consistent with the runtime abstraction layer.
  • Target resolution priority (--target > stdin .cwd > getRepoRoot) is clearly documented and well-tested.
  • README.md / README.ja.md both updated, and both English and Japanese .mdx docs created — fully compliant with i18n rules.
  • No new dependencies introduced — no license contamination risk.
  • The || true in the hook command is appropriate for a non-critical hook; documented behaviour is correct.

Summary

The main actionable items are the orphaned JSDoc (#1), the potentially fragile ordering test (#2), and the missing positive-path copy test (#3). The realpath suggestion (#4) is a lower-priority correctness improvement. Everything else is minor cleanup.

- Move orphaned JSDoc comment to correct position above createCopyTestContext
- Fix withConcurrencyLimit test ordering fragility by sorting results
- Add positive-path test verifying copyFile is called in non-dry-run mode
- Remove redundant manual consoleErrorSpy.mockRestore() calls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Owner Author

@kexi kexi left a comment

Choose a reason for hiding this comment

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

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook


Task 1: Product Manager Review

Business Value: HIGH. This PR introduces vibe copy as a standalone command, decoupled from vibe start. This is a smart product move because:

  • It enables integration with third-party tools (Claude Code) via hooks, expanding the product's ecosystem reach.
  • It follows the Unix philosophy of composability — one command, one purpose.
  • It solves a real pain point: when a worktree already exists but files from the main worktree need to be re-synced.

User Experience: GOOD. The target resolution priority (--target > stdin > cwd) is well-designed for multiple usage modes (manual, hook, programmatic). The --quiet flag combined with || true in the hook config ensures silent operation in automated flows. The error message "Not in a worktree" is clear.

Strategic Alignment: The Claude Code integration via PostToolUse hook is strategically valuable — it positions vibe as a first-class companion for AI coding assistants. Having a .claude/settings.json shipped with the project is a good demonstration for users.

No actionable issues found.


Task 2: Developer Review

Code Quality & Maintainability: EXCELLENT.

  1. Refactoring is clean: The extraction of copyFiles, copyDirectories, withConcurrencyLimit, and resolveCopyConcurrency from start.ts into copy-runner.ts is well-executed. Both start.ts and copy.ts now share the same logic without duplication.

  2. API design is good: The copyFiles and copyDirectories functions now accept string[] patterns instead of VibeConfig, making them more reusable and testable. This is the right level of abstraction.

  3. Coding standards: The code follows the project's established patterns:

    • Named boolean conditions (e.g., const isMain = ..., const hasNoCopyConfig = ...) per CLAUDE.md
    • Early returns throughout
    • Dependency injection via AppContext
    • Proper use of OutputOptions for log control
  4. Minor observationcopy-runner.ts uses console.warn directly on line 143 (console.warn(\[vibe] Copy strategy: ...`)), but the rest of the codebase uses warnLog(). This is in a debug-only code path (VIBE_DEBUG), so it's consistent with the original start.ts` behavior. Not a blocker, but worth noting for future cleanup.

  5. The readTargetFromStdin function is well-implemented with:

    • TTY detection to avoid blocking on interactive terminals
    • Size limit (1 MB) to prevent resource exhaustion
    • Path validation via validatePath() and isAbsolute() check
    • Graceful fallback on any error

Performance & Scalability: No concerns. The concurrency control is inherited from the existing implementation.

No blocking issues.


Task 3: Quality Engineer Review

Test Coverage: GOOD overall.

  • copy.test.ts (472 lines) covers:

    • Main worktree detection (3 cases)
    • No config scenario
    • --target option
    • --dry-run mode (2 cases)
    • Actual file copy execution
    • stdin target resolution (6 cases: valid JSON, invalid JSON, empty, missing cwd, --target override, terminal)
  • copy-runner.test.ts covers withConcurrencyLimit (5 cases)

  • All 372 tests pass.

Missing test coverage (recommendations):

  1. --target with relative path: Currently --target does not validate the path with validatePath(). The stdin path goes through validatePath(), but the --target CLI argument does not. This is an inconsistency. While --target is a locally-provided argument (less risk than stdin), adding validatePath() for defense-in-depth would be consistent. See Security section below.

  2. No test for readTargetFromStdin with oversized payload: The 1 MB limit is defined but not tested. A test that sends >1 MB and verifies undefined is returned would be valuable.

  3. No test for --target with relative path: What happens when --target ./some-path is provided? resolve() will normalize it relative to CWD, but this behavior isn't tested.

  4. No integration test for the vibe start refactoring: The refactoring changed start.ts to use the shared copy-runner.ts functions. While existing start.test.ts tests pass, there are no explicit tests verifying the refactored runCopyAndPostHooks still invokes the shared functions correctly.

Regression Risk: LOW. The refactoring preserves the original function signatures and behavior. The start.test.ts import update from ./start.ts to ../utils/copy-runner.ts for resolveCopyConcurrency confirms the move was tracked.


Task 4: Security Engineer Review

  1. stdin input handling (GOOD):

    • TTY check prevents blocking on interactive terminals
    • 1 MB size limit prevents memory exhaustion
    • JSON.parse is wrapped in try/catch
    • Path is validated via isAbsolute() and validatePath() (null byte, newline, command substitution checks)
  2. --target option lacks validatePath() call (LOW RISK):

    • The --target CLI argument is provided by the local user (not untrusted input like stdin), so the risk is minimal.
    • However, for defense-in-depth consistency with stdin handling, consider adding validatePath(target) before using the --target value. This is especially relevant if --target is ever used programmatically.
  3. Path traversal via glob patterns (EXISTING RISK, NOT INTRODUCED BY THIS PR):

    • The expandCopyPatterns and expandDirectoryPatterns functions use patterns from .vibe.toml, which is already protected by the SHA-256 trust mechanism.
  4. Hook command in .claude/settings.json (ACCEPTABLE):

    • vibe copy --quiet 2>/dev/null || true is a safe command. The 2>/dev/null || true suppression is appropriate for a hook that shouldn't block the user workflow.
    • The timeout: 120 is reasonable for large CoW copy operations.
  5. No shell string execution (GOOD): All subprocess invocations use spawn with argument arrays.

No critical security issues.


Summary

This is a well-executed PR. The refactoring is clean, the new command follows established patterns, tests are comprehensive, and security considerations are well-addressed. The Claude Code integration is a smart strategic addition.

Verdict: APPROVE with the following minor suggestions:

  1. Consider adding validatePath() for the --target option for defense-in-depth consistency
  2. Consider adding a test for the 1 MB stdin size limit
  3. Consider adding a test for --target with a relative path

@claude
Copy link

claude bot commented Feb 21, 2026

Code Review

All CI checks pass (lint, typecheck, tests, e2e, docs, builds on all platforms). Here are my findings.

Summary

This PR cleanly adds vibe copy as a standalone subcommand and refactors shared copy logic into utils/copy-runner.ts. The hook integration with Claude Code's PostToolUse(EnterWorktree) is a well-designed feature. Overall the implementation is solid.


Strengths

  • Clean refactoring: Extracting copyFiles, copyDirectories, and withConcurrencyLimit from start.ts into copy-runner.ts eliminates duplication without changing behavior.
  • Security: Good defenses around stdin — 1 MB size cap, isAbsolute() check, and validatePath() call before using untrusted input.
  • Stdin fallback chain: The priority order (--target → stdin .cwdgetRepoRoot()) is logical and well-documented. Interactive use is correctly skipped via isTerminal().
  • Test coverage: Edge cases for stdin (empty, invalid JSON, no cwd field, terminal, --target override) are all tested.
  • i18n: Both English and Japanese docs and READMEs are updated in sync.
  • Bug fix in the move: console.warn calls and process.env.VIBE_DEBUG direct access in the original code are properly replaced with warnLog and ctx.runtime.env.get() in copy-runner.ts.

Issues / Suggestions

validatePath return value not captured (copy.ts:649)

validatePath(cwd);
return cwd;

If validatePath is a pure throw-based validator (throws on bad input, returns void), this is correct. But if it returns a sanitized/resolved path, the raw cwd is returned instead of the validated one. Worth a quick verification.

CopyService.prototype spy may be fragile (copy.test.ts:463)

const copyFileSpy = vi.spyOn(CopyService.prototype, 'copyFile').mockResolvedValue(undefined);

This works as long as getCopyService(ctx) returns a CopyService instance using prototype methods. If the service is a plain object or factory-wrapped, the spy won't intercept. Consider whether mocking via ctx.runtime.fs.copyFile (like line 449) would be more robust and consistent with the rest of the file.

Hook silences all stderr (settings.json:15)

vibe copy --quiet 2>/dev/null || true

2>/dev/null suppresses all stderr, making debugging silently broken hooks difficult. Since --quiet already suppresses non-essential output, and || true handles the 'vibe not installed' case, the stderr redirect seems redundant. Consider whether vibe copy --quiet || true is sufficient, or at least document why full stderr suppression is needed.

Mermaid diagram shows sequential steps as parallel arrows (commands/copy.mdx)

Steps D, E, F (read stdin, load config, copy files) are sequential but fan out from C, implying parallelism. Minor doc nit.

Confirm --quiet is already in parseArgsOptions

The diff adds --target to parseArgsOptions in main.ts but doesn't show --quiet being added. This is likely already a global option — just worth confirming args.quiet resolves correctly for the copy case.


No concerns on

  • License: No new dependencies introduced.
  • README/docs sync: Both language versions updated correctly.
  • withConcurrencyLimit recursive pattern: Functionally correct; async recursion avoids stack overflow concerns.
  • resolve() for path comparison: Acceptable — symlink edge cases are unlikely in this context.
  • EnterWorktree stdin format: The {"cwd": "..."} assumption is reasonable and gracefully falls back to getRepoRoot() on any parse failure.

Minor nit

The test "exits with error when --target points to main worktree path" (lines 372-384) sets both cwd and mainWorktreePath to /tmp/main-repo with isMainWorktree: true. The test at lines 386-397 already covers the more specific case (secondary worktree, --target = main path). The two overlap slightly; consider whether the first adds distinct coverage.


Overall this is well-implemented. The main item worth a second look before merging is the validatePath return value question.

- Add isAbsolute() and validatePath() for --target option (defense-in-depth)
- Change console.warn to warnLog for debug output consistency
- Add tests for stdin 1MB size limit, relative path rejection, null byte
  detection, and source/destination path verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 21, 2026

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook

Overall this is a clean, well-structured PR. The refactoring in start.ts is a clear improvement and the new copy command is implemented with good security practices.


Positives

  • Good refactoring: Extracting copyFiles, copyDirectories, resolveCopyConcurrency, and withConcurrencyLimit into copy-runner.ts removes duplication and makes the shared logic reusable. The new signatures (filePatterns: string[] instead of config: VibeConfig) are cleaner.
  • Security-conscious stdin handling: The readTargetFromStdin function correctly validates the path is absolute, calls validatePath() for null-byte/traversal checks, enforces a 1 MB size limit, and bails out on terminal stdin. All inside a try-catch for safe fallback.
  • process.env replaced with context abstraction: copy-runner.ts uses ctx.runtime.env.get("VIBE_DEBUG") instead of the old process.env.VIBE_DEBUG in start.ts. Consistent with the runtime abstraction layer.
  • console.warnwarnLog: Consistent with the output utility pattern.
  • Documentation: Both English and Japanese docs created (copy.mdx and ja/commands/copy.mdx), README updated in both languages. i18n sync rules followed.
  • Test coverage: 17 test cases covering main-worktree detection, dry-run, stdin edge cases (invalid JSON, empty, terminal, oversized, missing cwd), path validation, and copy path correctness.
  • License: No new external dependencies introduced.

Issues / Suggestions

Minor: --target option is accepted silently by all commands

In main.ts, --target is added to the shared parseArgsOptions object, meaning vibe start --target /foo or vibe clean --target /foo won't error. This is consistent with the existing pattern in the codebase (all options are shared), but it could be confusing for users. No action needed unless the project wants stricter per-command validation.

Minor: Inconsistent ordering of vibe copy across surfaces

The command appears in different positions depending on where you look:

  • main.ts help text (line 71): placed between vibe jump and vibe clean
  • README.md table: placed after vibe clean
  • docs/commands/index.mdx: placed after vibe start, before vibe clean

Worth making these consistent. The help text / docs position (after start, before clean) seems more logical since copy is conceptually related to start.

Minor: Missing test for "stdin provides main worktree path → should error"

There's a test for --target pointing to the main worktree path (line 386 in copy.test.ts), but no equivalent test for when the stdin JSON provides the main worktree path as cwd. The code handles this correctly (the isMain check runs after target resolution regardless of source), but a dedicated test would make intent explicit.

Observation: 2>/dev/null in hook suppresses copy failures

"command": "vibe copy --quiet 2>/dev/null || true"

--quiet already suppresses non-essential output from vibe. Adding 2>/dev/null additionally discards any stderr, including genuine error messages (e.g., permission errors, disk full). Users debugging why files didn't get copied in hook mode will see nothing.

This is a conscious tradeoff for non-blocking UX, and the docs explain || true ensures the hook doesn't fail if vibe isn't installed. It's worth a brief doc note that copy failures during hook execution are silently ignored, and users can run vibe copy manually to diagnose issues.

Nit: Standalone mode behavior is clear but implicit

The fallback in target resolution (getRepoRoot(ctx)) returns the current repo root, which is correct when running from a secondary worktree. The isMain guard correctly rejects attempts to run from the main worktree. The behavior is sound; the comment // standalone mode in the code is accurate.


Summary

Category Status
Code quality Good — clean refactoring, consistent style
Correctness Good — target resolution logic is sound
Security Good — stdin validated, path checked, size-limited
Tests Good — edge cases covered; one minor missing scenario
Docs Complete — EN + JA, README updated
License No issues

The minor issues above are all non-blocking. The most actionable one is the vibe copy ordering inconsistency in README vs help text.

- Align vibe copy ordering in README (EN/JA) with help text
- Fix Mermaid diagram to show sequential flow instead of parallel
- Add test for stdin providing main worktree path as cwd

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 21, 2026

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook

Overall this is a well-structured PR. The refactoring of shared copy logic into copy-runner.ts is clean, the new vibe copy command is thoughtfully designed, and the test coverage is solid. Here are my findings:


Code Quality & Architecture

Strengths:

  • Clean extraction of copyFiles, copyDirectories, withConcurrencyLimit, and resolveCopyConcurrency from start.ts into utils/copy-runner.ts. The refactored start.ts now delegates to the shared module with minimal changes.
  • The copyCommand signature change from VibeConfig to string[] patterns in copyFiles/copyDirectories is a good generalization that decouples the shared utilities from config structure.
  • Good use of the existing runtime abstraction (ctx.runtime.env.get(), ctx.runtime.io.stdin, etc.) instead of direct process access.
  • Target resolution priority (--target > stdin > repo root) is well-documented and tested.

Minor observations:

  1. copy.ts:131resolveCopyConcurrency(config, ctx) is called even when there are no directories to copy (files-only config). This is harmless but does unnecessary work. Consider moving the concurrency resolution into the copyDirectories function or just before it's needed.

  2. copy.ts:145 / copy.ts:147config.copy?.files ?? [] and config.copy?.dirs ?? [] are repeated here after already checking hasNoCopyConfig at line 124. At this point config is guaranteed to be non-null with at least one non-empty array. The null coalescing is defensive but slightly redundant given the guard above.


Security

Strengths:

  • Path validation via validatePath() on both --target and stdin cwd inputs.
  • Null byte and newline injection prevention.
  • Absolute path requirement for --target.
  • 1 MB stdin size limit to prevent resource exhaustion.
  • Terminal detection to skip stdin reading in interactive mode.

Potential concern:
3. copy.ts:37 — When stdin exceeds MAX_STDIN_SIZE, the function returns undefined silently, which falls through to getRepoRoot(). This is probably fine for the hook use case, but a debug/verbose log here would help diagnose issues where the hook's stdin payload is unexpectedly large. Currently there's no signal at all that the limit was hit.

  1. Path traversal via join()copyFiles and copyDirectories in copy-runner.ts use join(originPath, file) and join(targetPath, file) where file comes from glob expansion of config patterns. The expandCopyPatterns function is the trust boundary here. If a .vibe.toml contains files = ["../../etc/passwd"], join() would resolve it. This is mitigated by the fact that .vibe.toml requires trust verification, but it's worth noting that this is the same trust model as vibe start (no regression).

Potential Issues

  1. readTargetFromStdin timeout/hang risk — If stdin is a pipe that never sends EOF (e.g., a misbehaving hook), the while loop at copy.ts:33 will block indefinitely. The hook has a 120s timeout in the settings.json configuration, which provides an external safeguard, but the function itself has no internal timeout. This is acceptable given the hook context, but worth documenting.

  2. copy.ts:111resolve(originPath) === resolve(targetPath) uses node:path.resolve() which does purely lexical path resolution (no symlink following). If the main worktree path contains symlinks, this comparison could produce false negatives. Consider whether fs.realPath() should be used here for robustness (the mock context already has realPath available).


Test Coverage

Strengths:

  • 552 lines of tests for the copy command covering: main worktree detection, stdin resolution (valid JSON, invalid JSON, empty, terminal, oversized, no cwd field), --target validation (relative path, null byte), dry-run mode, actual copy execution, and source/destination path verification.
  • withConcurrencyLimit tests cover concurrency enforcement, empty arrays, oversized limits, and index passing. Good use of sort() to handle non-deterministic ordering.

Gaps:
7. No error propagation test — There's no test verifying behavior when getMainWorktreePath() or getRepoRoot() throws (e.g., not in a git repo at all). The catch-all at copy.ts:165 handles this, but it's untested.

  1. No test for directory copy — All copy tests use files = [...] config. There's no test exercising the dirs path through copyCommand. While copyDirectories is tested via start.ts tests and the shared copy-runner.ts, an integration test through copyCommand with dirs config would be valuable.

  2. consoleWarnSpy in createCopyTestContext is created but never asserted against in any test. Consider removing it or adding assertions for warning output in relevant tests.


Documentation

  • English and Japanese docs are properly synchronized with matching structure.
  • README.md and README.ja.md both updated with the new command entry in the correct table position.
  • Mermaid diagrams used appropriately per project conventions.
  • Help text in main.ts updated consistently.
  • Command index pages (EN/JA) updated.

All looks good here.


License Compatibility

No new dependencies introduced. The PR only reorganizes existing code and adds new files using existing internal modules. No license concerns.


Hook Configuration (.claude/settings.json)

  1. 2>/dev/null suppresses all stderr — The hook command vibe copy --quiet 2>/dev/null || true means all error messages (including security validation failures like null bytes in paths) are silently discarded. This is intentional for a non-interactive hook context, but could make debugging difficult. The docs mention this trade-off, which is good. Users who need to debug can remove 2>/dev/null.

Summary

This is a solid PR with good security practices, thorough test coverage, and proper documentation. The refactoring is clean and doesn't break the existing start command's behavior. The main areas for potential improvement are:

  • Consider adding fs.realPath() for worktree path comparison (item 6)
  • Add a verbose/debug log when stdin exceeds the size limit (item 3)
  • Consider adding a directory copy integration test through copyCommand (item 8)

These are suggestions, not blockers.

- Use realPath() instead of resolve() for symlink-aware main worktree detection
- Add warnLog for stdin payload exceeding 1MB size limit
- Remove unused resolve import from node:path
- Add test for directory copy with dirs config
- Import warnLog from output.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 21, 2026

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook

Overall Assessment

Well-structured PR that cleanly extracts shared copy logic into a reusable module and adds a new vibe copy subcommand. The refactoring is clean and the new command follows existing patterns in the codebase. Here's detailed feedback:


Code Quality & Architecture

Strengths:

  • Clean extraction of copyFiles, copyDirectories, and withConcurrencyLimit into utils/copy-runner.ts — good separation of concerns
  • copyCommand follows the same patterns as other commands (context injection, OutputOptions, error handling)
  • Target resolution priority (--target > stdin > repo root) is well-documented and logical
  • The resolveCopyConcurrency function cleanly consolidates env/config/default precedence

Suggestions:

  1. copy.ts:72 — Silent error swallowing in readTargetFromStdin: The catch-all catch {} block at line 72 silently swallows all errors, including validatePath throwing on malicious input (null bytes, command substitution). While returning undefined falls back to repo root (which is safe), consider logging at verbose level so debugging hook issues isn't opaque. This would require threading OutputOptions into the function, so it may not be worth the plumbing — but worth considering.

  2. copy.ts:100exit(1) + return pattern: After calling ctx.runtime.control.exit(1) at line 100, the code does return to prevent further execution. This is consistent with the rest of the codebase (start.ts has the same pattern), so no change needed — just noting the dual-guard pattern.

  3. copy-runner.ts:63-69withConcurrencyLimit execution order: The recursive executeNext() pattern means items may complete out of order. This is fine for independent copy operations, but worth noting in the JSDoc that items may complete out of order.


Security

Strengths:

  • validatePath() checks for null bytes, newlines, command substitution patterns — good defense-in-depth
  • isAbsolute() check on both --target and stdin cwd prevents relative path traversal
  • MAX_STDIN_SIZE (1MB) prevents resource exhaustion from stdin
  • realPath() for symlink-aware main worktree detection prevents bypass via symlinks

No concerns found. The stdin input handling is appropriately defensive. No new shell string execution is introduced — copyService uses Node.js filesystem APIs directly.


Test Coverage

Strengths:

  • 598 lines of tests for copy.ts covering: main worktree detection, --target validation, stdin resolution (valid JSON, invalid JSON, empty, no cwd, terminal, oversized, main worktree path), dry-run mode, actual copy execution, directory copy, and path verification
  • copy-runner.test.ts covers withConcurrencyLimit including edge cases (empty array, limit > items, index passing)
  • The createCopyTestContext helper is well-structured and reusable

Potential gaps:

  • No test for resolveCopyConcurrency edge cases (e.g., VIBE_COPY_CONCURRENCY=0, VIBE_COPY_CONCURRENCY=-1, VIBE_COPY_CONCURRENCY=33, non-numeric string). The function handles these, but they're untested. Consider adding a few cases in copy-runner.test.ts.
  • No test for what happens when realPath() fails (e.g., target path doesn't exist yet). In the current code, this would throw and be caught by the outer try/catch — which is correct, but a test would document this behavior.
  • No test for stdin with a relative path in .cwd (the isAbsolute check at line 66-67). There's a test for --target relative path but not for stdin relative path.

Performance

No concerns. The command reuses the existing withConcurrencyLimit pattern and CopyService (CoW) infrastructure. The stdin reading is efficient with 4KB buffer chunks and a 1MB cap.


Documentation

Both English and Japanese docs are well-synchronized with identical structure. The Mermaid diagram clearly illustrates the hook flow. The comparison table with vibe start is helpful.

README.md and README.ja.md are both updated with the new command in the command table.

The command index pages (commands/index.mdx and ja/commands/index.mdx) both include vibe copy.

Minor note: The jump command is missing from both commands/index.mdx command tables (pre-existing issue, not introduced by this PR).


License Compatibility

No new dependencies are introduced. The PR only adds new source files and refactors existing code. No license concerns.


.claude/settings.json Hook

The hook configuration is reasonable:

  • --quiet suppresses output during automated hook execution
  • 2>/dev/null suppresses stderr (e.g., if vibe is not installed)
  • || true ensures the hook always succeeds
  • 120s timeout is generous but appropriate for large node_modules CoW copies

One consideration: This file is checked into the repo. Users who don't have vibe installed will have a harmless no-op hook, which is fine. But users who use Claude Code without wanting automatic copies might be surprised. The || true makes it safe, and the [copy] config must exist in .vibe.toml for anything to happen, so this is acceptable.


Summary

This is a solid PR. The main actionable suggestions are:

  1. Add test cases for resolveCopyConcurrency edge cases and stdin relative-path rejection
  2. Consider verbose-level logging in the readTargetFromStdin catch block for debuggability

The code is clean, secure, well-documented, and follows existing codebase patterns.

- Move resolveCopyConcurrency to only run when dirs config exists
- Add error propagation test for git command failures
- Add consoleWarnSpy assertion in stdin 1MB size limit test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 21, 2026

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook

Overview

This PR adds a vibe copy subcommand that copies files/directories from the main worktree to a secondary worktree using CoW, based on .vibe.toml [copy] configuration. It also extracts shared copy logic from start.ts into a reusable copy-runner.ts module. The architecture is clean — the refactoring is well-scoped, the new command follows existing patterns, and the Claude Code hook integration is a practical addition.


Code Quality & Architecture

Strengths:

  • The extraction of copyFiles, copyDirectories, withConcurrencyLimit, and resolveCopyConcurrency into copy-runner.ts is a clean refactoring. Both start.ts and copy.ts now share the same infrastructure without duplication.
  • The function signatures were improved during extraction — accepting filePatterns: string[] / dirPatterns: string[] instead of the full VibeConfig object, which makes them more composable.
  • Target resolution priority (--target > stdin > repo root) is well-documented and logically structured.
  • Uses the runtime abstraction layer correctly (ctx.runtime.env.get(), ctx.runtime.fs.realPath(), ctx.runtime.io.stdin) instead of direct process.env / process.stdin access.

Minor observations:

  • In start.ts:464, resolveCopyConcurrency is called unconditionally inside the !skipCopy block, even when there are no directories to copy. Compare with copy.ts:152-154 which gates it behind hasDirs. This inconsistency is functionally harmless (the value goes unused), but aligning them would be cleaner.
  • copy.ts:150 accesses config.copy?.files with optional chaining despite the hasNoCopyConfig guard at line 130 already checking this. This is defensive coding and is fine, but the guard already ensures config has copy config at this point.

Security

Well-handled:

  • stdin path validation: isAbsolute() check + validatePath() (null byte, newline, command substitution detection) + 1MB size limit — all defense-in-depth.
  • --target option also gets isAbsolute() + validatePath() for consistency.
  • realPath() used to resolve symlinks before main worktree comparison (e.g., /tmp/private/tmp on macOS).

One consideration:

  • realPath on targetPath (copy.ts:116) will throw if the path doesn't exist. This is caught by the outer catch block and results in a clean error exit, so it's acceptable. But a more specific error message ("target path does not exist") could improve debuggability vs. the generic "Error: ENOENT" the user would see.

Performance

  • No concerns. The copy infrastructure reuses the existing CoW and concurrency-limited patterns.
  • stdin reading uses a reasonable 4KB buffer with a 1MB cap, which is appropriate for the hook JSON payload use case.

Test Coverage

Good coverage (625 lines for copy command, 51 for copy-runner):

  • Main worktree detection (from cwd, from --target, from stdin)
  • --target validation (relative path, null byte)
  • stdin edge cases: invalid JSON, empty, no cwd field, terminal stdin, oversized payload, main worktree path via stdin
  • Dry-run mode (no actual copies, output verification)
  • Positive path tests (file copy called with correct paths, directory copy)
  • Git failure propagation
  • withConcurrencyLimit: correctness, limit enforcement, edge cases (empty array, limit > items)

Suggestions for additional coverage:

  • The readTargetFromStdin function's early return on exceedsMaxSize doesn't drain remaining stdin bytes. While this is fine for a short-lived process, a test verifying the command completes cleanly (no hang) with a very large stdin could be valuable.
  • No test for the case where realPath fails on target (e.g., nonexistent target directory). This would exercise the catch block's error reporting.

Documentation

  • Both English and Japanese docs for the copy command are provided and structurally identical (packages/docs/src/content/docs/commands/copy.mdx and ja/commands/copy.mdx).
  • Command index pages (commands/index.mdx) updated in both languages.
  • Both README.md and README.ja.md updated with the new command in the command table.
  • Mermaid diagrams used instead of ASCII art, following the project's markdown rules.
  • Help text in main.ts updated.

All documentation requirements appear met.


License Compatibility

No new dependencies were introduced. The change only reorganizes existing code and adds new source files. No license concerns.


.claude/settings.json Hook

The PR adds a PostToolUse(EnterWorktree) hook to the repo-level .claude/settings.json. This means all contributors using Claude Code in this repo will automatically run vibe copy --quiet 2>/dev/null || true on worktree entry. The || true and 2>/dev/null ensure it's non-disruptive if vibe is not installed, which is a good safeguard.


Summary

This is a well-structured PR. The refactoring is clean, security is handled thoroughly, test coverage is solid, and documentation is complete in both languages. The items noted above are minor and non-blocking.

- Add 10 tests for resolveCopyConcurrency: default, config, env override,
  boundary values (1, 32), invalid values (0, -1, 33, non-numeric)
- Add test for stdin cwd with relative path rejection
- Align start.ts to only call resolveCopyConcurrency when dirs exist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 22, 2026

PR Review: feat: add vibe copy subcommand with Claude Code worktree hook

Overall this is a well-structured PR. The refactoring of shared copy logic into copy-runner.ts is clean, the new vibe copy command follows existing codebase patterns, and the security considerations are thorough. Below are specific observations.


Code Quality & Design

Positives:

  • Clean extraction of copyFiles, copyDirectories, withConcurrencyLimit, and resolveCopyConcurrency into utils/copy-runner.ts — good separation of concerns for reuse.
  • The CopyOptions interface properly extends OutputOptions, consistent with other commands.
  • Target resolution priority (--target > stdin > repo root) is well-documented in both code comments and docs.
  • The copy-runner.ts refactoring improved VIBE_DEBUG usage from process.env.VIBE_DEBUG to ctx.runtime.env.get("VIBE_DEBUG") — more consistent with the runtime abstraction pattern. (Note: the rest of the codebase in runtime/node/native.ts still uses process.env.VIBE_DEBUG directly, but that is in the native runtime layer where it is appropriate.)

Minor suggestions:

  1. copy.ts:150config.copy?.files ?? [] and config.copy?.dirs ?? [] are evaluated after the hasNoCopyConfig guard already confirmed at least one is non-empty. The ?? [] fallback is redundant at that point, though it is defensive and does not hurt.

  2. copy.ts:152 — The hasDirs guard before copyDirectories is a nice optimization to skip resolveCopyConcurrency when unnecessary. The same pattern was applied to start.ts:463 — good consistency.

  3. copy-runner.ts signature change — The functions now accept filePatterns: string[] / dirPatterns: string[] instead of the full VibeConfig object. This is a better API design since the functions only need the pattern arrays, not the entire config.


Potential Issues

  1. stdin blocking riskreadTargetFromStdin reads stdin in a loop until EOF. If invoked in a context where stdin is a pipe that never closes (not a terminal, but also not sending EOF), the command will block indefinitely. The isTerminal() check handles interactive sessions, and the hook timeout (120s) provides an upper bound in the Claude Code context. But standalone usage like some-command | vibe copy where the piping process hangs could be problematic. Consider documenting this or adding a read timeout.

  2. realPath on non-existent targetcopy.ts:116 calls ctx.runtime.fs.realPath(targetPath) to compare origin and target. If the target path does not exist yet (e.g., race condition in worktree creation), realPath may throw. This would be caught by the outer try-catch and shown as a generic error. Worth considering whether a more specific error message would be helpful here.

  3. validatePath throws in --target path — At copy.ts:103, if validatePath(target) throws (e.g., for shell metacharacters), the error is caught by the outer try-catch at line 174 and displayed as Error: <message>. This works correctly, but the error message might be slightly confusing since it comes from the validation utility rather than a copy-specific message. This is minor and acceptable.


Security

The security posture is strong:

  • stdin input validation: Absolute path check + validatePath() (null bytes, newlines, command substitution) + 1 MB size limit — good defense-in-depth against untrusted input.
  • --target validation: Same checks applied consistently.
  • Symlink resolution: realPath before origin/target comparison prevents symlink-based bypass of the main worktree check.
  • Config loaded from main worktree: A secondary worktree cannot inject malicious configuration.
  • Path traversal prevention: Delegated to expandCopyPatterns/expandDirectoryPatterns which already reject .. patterns.

No security concerns identified.


Performance

  • The hasDirs guard avoids unnecessary resolveCopyConcurrency computation when there are no directories — minor optimization.
  • Copy operations use the existing CoW-optimized CopyService with concurrency control — no new performance concerns.
  • stdin reading uses a reasonable 4096-byte buffer with 1 MB cap — appropriate for the expected JSON payload size.

Test Coverage

Tests are comprehensive with 641 lines covering:

  • Main worktree detection (cwd, --target, stdin all pointing to main)
  • --target validation (relative path, null byte injection)
  • stdin parsing (valid JSON, invalid JSON, empty, no cwd field, relative path, terminal, oversized payload)
  • dry-run mode (no copy calls, dry-run output)
  • File and directory copying with correct paths
  • Git command failures

copy-runner.test.ts adds solid coverage for withConcurrencyLimit and resolveCopyConcurrency.

Gaps to consider:

  • No test for validatePath throwing on shell metacharacters (e.g., $(cmd)) in --target — only null byte is tested. The validatePath function itself is presumably tested elsewhere, but an integration test showing the error path in copyCommand would add confidence.
  • No test for individual file/directory copy failure warnings (the warnLog path in copyFiles/copyDirectories).

Documentation

  • README.md and README.ja.md both updated with the new vibe copy command entry — properly synchronized.
  • New copy.mdx documentation is thorough: usage, options, examples, mermaid diagram, target resolution explanation, Claude Code integration setup, comparison with vibe start.
  • Japanese version (ja/commands/copy.mdx) is well-translated and structurally identical.
  • Commands index updated in both English and Japanese versions.
  • Badge versions consistently show v0.23.0+.

License

No new dependencies introduced. All changes are internal code refactoring and new command implementation. No license concerns.


.claude/settings.json Hook

The hook configuration is sensible:

  • "matcher": "EnterWorktree" — correctly scoped to worktree operations
  • "command": "vibe copy --quiet 2>/dev/null || true"--quiet suppresses output, 2>/dev/null silences stderr, || true prevents hook failure if vibe is not installed
  • "timeout": 120 — reasonable upper bound for copy operations

One consideration: this hook is committed to the repo's .claude/settings.json, meaning all Claude Code users of this repo will have it active. This seems intentional given the project context (vibe is a worktree management tool), but worth noting that it assumes vibe is in PATH for all contributors.


Summary

This is a solid PR. The refactoring is clean, security considerations are thorough, test coverage is good, and documentation is well-synchronized across languages. The main items to consider are the stdin blocking edge case and the minor test coverage gaps noted above. Approving with minor suggestions.

@kexi kexi merged commit 27959d4 into develop Feb 22, 2026
21 checks passed
@kexi kexi deleted the feat/cow-copy-skill branch February 22, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant