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