* Post-creation setup for a newly created worktree. * Propagates settings.local.json, configures git hooks, and symlinks directories.
( repoRoot: string, worktreePath: string, )
| 510 | * Propagates settings.local.json, configures git hooks, and symlinks directories. |
| 511 | */ |
| 512 | async function performPostCreationSetup( |
| 513 | repoRoot: string, |
| 514 | worktreePath: string, |
| 515 | ): Promise<void> { |
| 516 | // Copy settings.local.json to the worktree's .claude directory |
| 517 | // This propagates local settings (which may contain secrets) to the worktree |
| 518 | const localSettingsRelativePath = |
| 519 | getRelativeSettingsFilePathForSource('localSettings') |
| 520 | const sourceSettingsLocal = join(repoRoot, localSettingsRelativePath) |
| 521 | try { |
| 522 | const destSettingsLocal = join(worktreePath, localSettingsRelativePath) |
| 523 | await mkdirRecursive(dirname(destSettingsLocal)) |
| 524 | await copyFile(sourceSettingsLocal, destSettingsLocal) |
| 525 | logForDebugging( |
| 526 | `Copied settings.local.json to worktree: ${destSettingsLocal}`, |
| 527 | ) |
| 528 | } catch (e: unknown) { |
| 529 | const code = getErrnoCode(e) |
| 530 | if (code !== 'ENOENT') { |
| 531 | logForDebugging( |
| 532 | `Failed to copy settings.local.json: ${(e as Error).message}`, |
| 533 | { level: 'warn' }, |
| 534 | ) |
| 535 | } |
| 536 | } |
| 537 | |
| 538 | // Configure the worktree to use hooks from the main repository |
| 539 | // This solves issues with .husky and other git hooks that use relative paths |
| 540 | const huskyPath = join(repoRoot, '.husky') |
| 541 | const gitHooksPath = join(repoRoot, '.git', 'hooks') |
| 542 | let hooksPath: string | null = null |
| 543 | for (const candidatePath of [huskyPath, gitHooksPath]) { |
| 544 | try { |
| 545 | const s = await stat(candidatePath) |
| 546 | if (s.isDirectory()) { |
| 547 | hooksPath = candidatePath |
| 548 | break |
| 549 | } |
| 550 | } catch { |
| 551 | // Path doesn't exist or can't be accessed |
| 552 | } |
| 553 | } |
| 554 | if (hooksPath) { |
| 555 | // `git config` (no --worktree flag) writes to the main repo's .git/config, |
| 556 | // shared by all worktrees. Once set, every subsequent worktree create is a |
| 557 | // no-op — skip the subprocess (~14ms spawn) when the value already matches. |
| 558 | const gitDir = await resolveGitDir(repoRoot) |
| 559 | const configDir = gitDir ? ((await getCommonDir(gitDir)) ?? gitDir) : null |
| 560 | const existing = configDir |
| 561 | ? await parseGitConfigValue(configDir, 'core', null, 'hooksPath') |
| 562 | : null |
| 563 | if (existing !== hooksPath) { |
| 564 | const { code: configCode, stderr: configError } = |
| 565 | await execFileNoThrowWithCwd( |
| 566 | gitExe(), |
| 567 | ['config', 'core.hooksPath', hooksPath], |
| 568 | { cwd: worktreePath }, |
| 569 | ) |
no test coverage detected