## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - A remote Claude ACP run stages the selected runtime skills into the sandbox. The host archives the materialized skills directory with tar and uploads it. > - Concurrent runs of one agent share that skills directory. The directory key is the selected skill set and its content. > - Each run calls `materializePaperclipSkillCopy` for each skill. The function always created and removed a sibling `<skill>.lock` directory, even when the copy was already current. > - The archive step lists the directory entries first and then gives the names to tar. When a concurrent run removed its lock between these two steps, tar failed with `Cannot stat` and the whole run failed as `adapter_failed`. > - This pull request checks the materialized fingerprint before it takes the lock. An up-to-date copy now leaves no transient entries in the shared directory. > - The benefit is that concurrent runs of one agent no longer fail each other's skill staging. ## Linked Issues or Issue Description **What happened?** A remote `claude_local` run failed before the agent started. The skill staging step ran `tar -c ... -T entries.nul` on the shared runtime-skills directory. Tar stopped with `tar: <skill>.lock: Cannot stat: No such file or directory` and exit status 2. Another run of the same agent was materializing the same skills at the same time. **Expected behavior** A run that finds its skills already materialized does not change the shared skills directory. Concurrent runs can archive that directory safely. **Steps to reproduce** 1. Materialize a skill into a skills directory. 2. Call `materializePaperclipSkillCopy` again in a loop for the same skill. 3. At the same time, call `createTarballFromDirectory` on the skills directory in a loop. 4. Before this change, most archives fail with `Cannot stat` on `<skill>.lock`. **Agent adapter(s) involved** `claude_local` (ACP, remote sandbox). The function is shared, so `grok_local` and the Codex and Gemini ACP skill paths also benefit. ## What Changed - `packages/adapter-utils/src/server-utils.ts`: `materializePaperclipSkillCopy` hashes the source and checks the materialized sentinel before it takes the lock. When the copy is current, it returns without creating the lock directory or a temporary copy. The existing check inside the lock stays, so a copy that is missing or out of date still uses the lock as before. The source is hashed once per call, the same as before. - `packages/adapter-utils/src/server-utils.test.ts`: a new test holds a live lock from another owner, then materializes an already-current skill. The call must return at once and must not change the lock or the directory. ## Verification - `cd packages/adapter-utils && npx vitest run src/server-utils.test.ts`: 132 passed. - Red/green: without the fix, the new test times out because the call waits on the live lock. With the fix, it passes. - `npx vitest run src/acpx-engine/execute.test.ts -t skill`: 15 passed. - `npx tsc --noEmit -p tsconfig.json` in `packages/adapter-utils`: passes. - Real-dependency race check (a temporary script, not committed). The script used real files and the system tar. It ran `materializePaperclipSkillCopy` for 5 skills in a loop while `createTarballFromDirectory` archived the same directory for 8 seconds: - Before the fix: 1994 of 2307 archives failed with `tar: skill-1.lock: Cannot stat: No such file or directory`. - After the fix: 0 of 2021 archives failed. - Assumption under test: the sentinel check outside the lock is safe. The sentinel is written into the temporary copy before the atomic rename, so a matching sentinel means a complete copy. The lock never protected readers: the caller already reads `SKILL.md` after the lock is released. - Remaining limit: the first materialization of a new skill set still uses the lock. A concurrent archive of the same directory is unlikely then, because a run archives the directory only after it has materialized every skill in it. ## Risks - Low risk. The change only skips the lock when the result is already correct. A missing, damaged, or out-of-date copy uses the same locked path as before. - No change to the materialized layout, the sentinel format, or the lock format. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use (shell, file edits, test runs). The exact context-window size is not exposed in this session. ## 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: devinfoley <139239+devinfoley@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@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.