Create `` /.claude/worktrees/agent- /`` as a detached worktree of ``base_dir``. Returns a :class:`WorktreePaths` with ``created=True`` on success. On any failure — bad session_id, not a git repo, mkdir error, git command error, subprocess timeout — falls back to ``ba
(
base_dir: str, session_id: str,
)
| 128 | |
| 129 | |
| 130 | async def create_agent_worktree( |
| 131 | base_dir: str, session_id: str, |
| 132 | ) -> WorktreePaths: |
| 133 | """Create ``<base>/.claude/worktrees/agent-<session_id>/`` as a |
| 134 | detached worktree of ``base_dir``. |
| 135 | |
| 136 | Returns a :class:`WorktreePaths` with ``created=True`` on success. |
| 137 | On any failure — bad session_id, not a git repo, mkdir error, git |
| 138 | command error, subprocess timeout — falls back to ``base_dir`` |
| 139 | with ``created=False`` and a warning log. Never raises. |
| 140 | """ |
| 141 | # Validate session_id before it touches the filesystem. The |
| 142 | # work-secret payload type is loose (server-provided string), so |
| 143 | # nothing else stops a malicious or buggy ``id`` value from |
| 144 | # turning into a path-traversal write target. |
| 145 | if not _SESSION_ID_RE.match(session_id): |
| 146 | logger.warning( |
| 147 | '[worktree] session_id %r is not allowlist-safe — ' |
| 148 | 'spawning in base dir', session_id, |
| 149 | ) |
| 150 | return WorktreePaths( |
| 151 | base_dir=base_dir, working_dir=base_dir, created=False, |
| 152 | ) |
| 153 | |
| 154 | if not await _is_git_repo(base_dir): |
| 155 | logger.warning( |
| 156 | '[worktree] %s is not a git repo — spawning in base dir', |
| 157 | base_dir, |
| 158 | ) |
| 159 | return WorktreePaths( |
| 160 | base_dir=base_dir, working_dir=base_dir, created=False, |
| 161 | ) |
| 162 | |
| 163 | target = os.path.join( |
| 164 | base_dir, '.claude', 'worktrees', f'agent-{session_id}', |
| 165 | ) |
| 166 | parent = os.path.dirname(target) |
| 167 | try: |
| 168 | os.makedirs(parent, exist_ok=True) |
| 169 | except OSError as err: |
| 170 | logger.warning( |
| 171 | '[worktree] mkdir(%s) failed: %s — spawning in base dir', |
| 172 | parent, err, |
| 173 | ) |
| 174 | return WorktreePaths( |
| 175 | base_dir=base_dir, working_dir=base_dir, created=False, |
| 176 | ) |
| 177 | |
| 178 | # ``--detach`` avoids polluting the branch namespace; the worktree |
| 179 | # starts on whatever commit the base dir's HEAD pointed at when |
| 180 | # we ran ``git worktree add`` (concurrent ``git reset`` is racy |
| 181 | # but acceptable for an ephemeral agent worktree). It has no |
| 182 | # branch attached, so ``--force`` removal stays clean later. |
| 183 | rc, stderr = await _run_git( |
| 184 | 'worktree', 'add', '--detach', target, 'HEAD', cwd=base_dir, |
| 185 | ) |
| 186 | if rc != 0: |
| 187 | logger.warning( |