Why this matters
setup() is the moment the process becomes tied to the current workspace.
It pins down the cwd, snapshots the hook config before anything can mutate it,
and then starts the background jobs the first render depends on.
Lock the cwd and capture hooks first
The order matters. setCwd(cwd) must happen before any code reads the current
directory, and the hooks snapshot has to happen before later startup work can
silently change what hooks are visible.
export async function setup(
cwd: string,
permissionMode: PermissionModeArg,
allowDangerouslySkipPermissions: boolean,
worktreeEnabled: boolean,
worktreeName?: string,
sessionId?: string,
worktreePRNumber?: number,
messagingSocketPath?: string,
) {
// IMPORTANT: setCwd() must be called before any other code that depends on the cwd
setCwd(cwd)
// Capture hooks configuration snapshot to avoid hidden hook modifications.
// IMPORTANT: Must be called AFTER setCwd() so hooks are loaded from the correct directory
const hooksStart = Date.now()
captureHooksConfigSnapshot()
logForDiagnosticsNoPII('info', 'setup_hooks_captured', {
duration_ms: Date.now() - hooksStart,
})
// Initialize FileChanged hook watcher — sync, reads hook config snapshot
initializeFileChangedWatcher(cwd)
Keep the project root stable
When setup creates or enters a worktree, it eventually makes the worktree path the session’s stable project root. That root is what later code uses for history, skills, sessions, and command loading.
process.chdir(worktreeSession.worktreePath)
setCwd(worktreeSession.worktreePath)
setOriginalCwd(getCwd())
// --worktree means the worktree IS the session's project, so skills/hooks/
// cron/etc. should resolve here. (EnterWorktreeTool mid-session does NOT
// touch projectRoot — that's a throwaway worktree, project stays stable.)
setProjectRoot(getCwd())
saveWorktreeState(worktreeSession)
// Clear memory files cache since originalCwd has changed
clearMemoryFileCaches()
// Settings cache was populated in init() (via applySafeConfigEnvironmentVariables)
// and again at captureHooksConfigSnapshot() above, both from the original dir's
// .claude/settings.json. Re-read from the worktree and re-capture hooks.
updateHooksConfigSnapshot()
setProjectRoot(getCwd()) makes the new worktree path the session’s project
identity. cwd may still move during startup, but projectRoot is the stable
anchor the rest of the app uses for project-scoped behavior such as command
loading, session lookup, and history.
Start the background jobs before first render
Once the workspace is concrete, setup turns on the small background pieces the first turn needs: session memory, command loading, sinks, and the startup analytics beacon.
if (!isBareMode()) {
initSessionMemory() // Synchronous - registers hook, gate check happens lazily
if (feature('CONTEXT_COLLAPSE')) {
/* eslint-disable @typescript-eslint/no-require-imports */
;(
require('./services/contextCollapse/index.js') as typeof import('./services/contextCollapse/index.js')
).initContextCollapse()
/* eslint-enable @typescript-eslint/no-require-imports */
}
}
void lockCurrentVersion() // Lock current version to prevent deletion by other processes
logForDiagnosticsNoPII('info', 'setup_background_jobs_launched')
profileCheckpoint('setup_before_prefetch')
logForDiagnosticsNoPII('info', 'setup_prefetch_starting')
const skipPluginPrefetch =
(getIsNonInteractiveSession() &&
isEnvTruthy(process.env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL)) ||
isBareMode()
if (!skipPluginPrefetch) {
void getCommands(getProjectRoot())
}
initSinks() // Attach error log + analytics sinks and drain queued events
// Session-success-rate denominator. Emit immediately after the analytics
// sink is attached — before any parsing, fetching, or I/O that could throw.
logEvent('tengu_started', {})
initSessionMemory() gets the session memory hook ready. void getCommands(getProjectRoot())
starts the command prefetch using the stable project root. main.tsx can begin
other prefetch work earlier, but this is the setup-owned version that depends
on the workspace now being concrete. initSinks() must happen before
logEvent('tengu_started', {}) so the event does not get dropped.
Takeaways
- setup() binds the process to the current workspace after global bootstrap finishes.
- The project root is stable, while cwd can move during startup worktree handling.
- Background jobs and analytics sinks are launched before the first real render.