## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A project can attach more than one repository. The task workspace keeps the selected repository at its root and puts the other project repositories under `.paperclip-repositories/<name>-<key>`, each with its own `.git` > - Agents can run on an SSH execution environment. Paperclip copies the task workspace to the remote host before the run and restores it after the run > - The SSH copy excludes `.git` at every depth, but the restore baseline excludes it only at the workspace root > - So the other project repositories reach the remote host without Git, and the restore then deletes their `.git` directories on the Paperclip host > - The next run of the same task fails during workspace setup, and the agent cannot commit to those repositories on the remote host > - This pull request transports each project repository as a Git workspace of its own, the same way the sandbox path already handles them > - The benefit is that multi-repository projects work on SSH environments across consecutive runs ## Linked Issues or Issue Description Refs #11632 (SSH workspace transfer exclude list). Related SSH workspace PRs: #14233, #14428, #14472. I found no issue or PR for this bug. **What happened?** A project has two repositories and its agent runs on an SSH environment. After the first run, the second repository under `.paperclip-repositories/` has no `.git` directory on the Paperclip host. The next run of the same task fails during setup with `Managed workspace path "…/.paperclip-repositories/<repo>" already exists but is not a git checkout.` On the remote host, `git` inside that repository resolves to the parent repository. **Expected behavior** Each project repository reaches the remote host as a Git checkout with its local changes. Remote commits and edits come back after the run. The next run of the same task starts normally. **Steps to reproduce** 1. Create a project with two repositories. 2. Configure an SSH execution environment and make it the agent's default environment. 3. Assign a task to the agent and let it run once. 4. Look at `.paperclip-repositories/<repo>` in the task workspace: `.git` is gone. 5. Wake the agent on the same task again: the run fails with `setup_failed`. **Paperclip version or commit** Reproduced on `v2026.916.1` and on `master` (`5edf55d73`). **Deployment mode** Self-hosted (Docker), authenticated, with an SSH execution environment. ## What Changed - `ssh.ts`: `prepareWorkspaceForSshExecution` lists the project repositories under `.paperclip-repositories/`. It applies the discovery rules of `readGitWorkspaceSnapshot`: each entry must be a directory with a valid name and must be a Git repository root, else the prepare step fails before any transfer. - `ssh.ts`: the anchor copy leaves `.paperclip-repositories/` out. Each project repository then gets the same import, sync, and deleted-path steps as the anchor. The remote anchor repository ignores `/.paperclip-repositories/`, as the local checkout does. - `ssh.ts`: `prepareWorkspaceForSshExecution` returns the transported repositories (the field is present only when there are repositories). `restoreWorkspaceFromSshExecution` accepts them with their baselines. It validates each path and baseline first, then restores the repositories before the anchor and stops at the first failure, as the sandbox restore does. - `remote-managed-runtime.ts`: the anchor baseline excludes `.paperclip-repositories/`, and each project repository gets its own baseline for the restore merge. - `ssh-fixture.test.ts`: regression tests for two consecutive managed runs and for the direct restore path, on a workspace with a project repository (commits, dirty edits, and a deleted file). Two tests for the new validation. - `docs/guides/board-operator/execution-workspaces-and-runtime-services.md`: one line about project repositories in the SSH round trip. ## Verification - The new regression test fails on `master` (`expected 'backend initial\n?? ../\n' to contain 'frontend initial'`) and passes with this change. - `PAPERCLIP_ENABLE_DARWIN_SSH_ENV_LAB=1 npx vitest run packages/adapter-utils/src/ssh-fixture.test.ts packages/adapter-utils/src/remote-managed-runtime.test.ts`: 32 passed, with the sshd fixture running. - `tsc --noEmit` passes for `packages/adapter-utils` and `server`, and `pnpm -r typecheck` passes for the other workspaces. The Rust step of `@paperclipai/paperclip-runner` did not run locally because `cargo` is not installed. - `node ./scripts/check-no-git-push.mjs` and `pnpm check:module-boundaries` pass. - `pnpm test:run` did not complete locally. Before it stopped, 5 tests failed: 2 in `server/src/__tests__/workspace-runtime.test.ts` and 3 in `server/src/__tests__/company-skills-service.test.ts`. The same 5 tests also fail on the base commit `5edf55d73` without this change. CI runs the full suite. - `pnpm build` passes for all workspaces except `@paperclipai/paperclip-runner` and `server`, because their build compiles the Rust runner binary and `cargo` is not installed. `tsc --noEmit` passes for `server`. - Manual test on a self-hosted `v2026.916.1` instance with the same change applied: a project with two repositories and an SSH environment. Two runs on the same task passed. After each run, the second repository keeps its `.git` on the host. On the remote host it is a Git checkout, and the remote anchor ignores it. ## Risks - Low risk. Workspaces without `.paperclip-repositories/` take the same path as before, and the return value is unchanged for them. - A workspace with an invalid entry under `.paperclip-repositories/` now fails the SSH prepare step. The sandbox path already rejects such entries. - If one repository fails to restore, the restore stops, as in the sandbox path. The remote run directory keeps the agent's work. - Each project repository adds one bundle import and one restore per run. The time grows with the number and size of the repositories. - Out of scope: other nested `.git` directories (for example a vendored checkout inside a repository) keep the existing SSH behavior. ## Model Used - Provider and model: Anthropic Claude Opus 5.5 (`claude-opus-5-5`), in Claude Code. - Capabilities: extended thinking, tool use, and code execution. The context window size was not recorded. - Use: the model investigated the bug, wrote the change and the tests, and ran the checks. A separate Claude Code agent reviewed the diff. The author reviewed the change. The manual test ran on the author's self-hosted instance. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
@paperclipai/adapter-utils
Shared utilities for Paperclip adapters: process spawning, environment injection, sandbox/SSH transport, workspace sync, and the round-trip helpers that move code between the local execution-workspace cwd and wherever the agent actually runs.
For the adapter-author guide see
docs/adapters/creating-an-adapter.md
and the in-repo notes at packages/adapters/AUTHORING.md.
Sandbox bridge deadlines
Command-managed bridge control operations (queue reads, input delivery, setup, and cleanup) have a host-enforced deadline of at most 30 seconds per shell command. A shorter configured timeout still applies. These operations cannot inherit the agent run's hours-long lifetime or wait forever on a provider that ignores its timeout. Long-lived agent session commands keep their own limits.
A control timeout reports a transport failure. It does not prove that the remote command stopped and does not replay an uncertain write. The process session closes failed input delivery and uses its existing shutdown path; normal execution settlement must still verify termination.
No-remote-git contract
The local execution-workspace cwd is the only persistence boundary across runs. No adapter may depend on a git remote for cross-run state.
Adapters that run the agent on a different host should use the SSH round-trip
helpers in src/ssh.ts:
prepareWorkspaceForSshExecution({ spec, localDir, remoteDir })— bundles the local cwd (tracked files, dirty edits, untracked additions, and the git history needed to reconstruct it) toremoteDirbefore the run starts. Runs with nogit remoteconfigured.restoreWorkspaceFromSshExecution({ spec, localDir, remoteDir, ... })— syncs the remote cwd back intolocalDirafter the run, including any new commits the agent created. Also runs with nogit remoteconfigured.
prepareRemoteManagedRuntime in
src/remote-managed-runtime.ts wraps both
calls for adapters that want a per-run remote workspace and an automatic
restoreWorkspace() finally hook.
The invariant is pinned by the no-remote-git contract case in
src/ssh-fixture.test.ts, which asserts that a
remote-only commit propagates to the local worktree through the
prepare → restore round-trip with no git remote configured at any point. Do
not regress that test.