paperclipai/paperclip · info
[paperclip] Workspace restore preserved local working tree…
Error message
[paperclip] Workspace restore preserved local working tree changes after clean sandbox restore.
What it means
resetLocalGitIndexToHead runs `git reset --quiet HEAD -- .` to unstage everything after a sandbox restore, asserts no staged changes remain (throws if staged diff is non-empty), then — only when checkWorkingTreeClean is set — runs `git diff --name-status HEAD`. Non-empty output means the restore left unstaged working-tree modifications, which the helper intentionally does NOT discard; this console.warn says those local changes were preserved through the clean-restore flow.
Solutions
- Inspect what survived: run `git status --porcelain` and `git diff` in the workspace directory.
- If you need a pristine tree, commit or stash the local changes, or explicitly clean them (git checkout -- . / git clean).
- Treat the message as expected behavior when local edits existed before the restore — data was deliberately preserved.
Defensive patterns
Strategy: validation
Validate before calling
import { execSync } from "node:child_process";
const dirty = execSync("git status --porcelain", { cwd: workspaceDir, encoding: "utf8" });
if (dirty.trim().length > 0) {
// decide before restore: commit, stash, or accept that restore preserves these edits
} Type guard
const isCleanStatus = (porcelain: string): boolean => porcelain.trim().length === 0;
Prevention
- Commit or stash local edits before triggering a clean sandbox restore if you want determinism.
- Read the warning as informational: restore preserves local work by design.
- After restore, run `git status --porcelain` to enumerate exactly what was kept.
When it happens
Trigger: A workspace that had local uncommitted edits (hotfixes, generated files) goes through a clean sandbox restore; the index resets to HEAD but the modified files remain on disk, so `git diff HEAD` still reports them.
Common situations: Agent run restored from a sandbox while a developer had local edits in the same checkout; generated artifacts differing from HEAD; end-of-line or permission-only diffs.
Related errors
- Invalid project repositories directory
- Project repository is not a Git checkout
- A full lowercase source SHA is required.
- codex_startup_trust_git_resolution_failed
- Could not read execution-target Git context
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-08-18).
Data as JSON: /api/errors/584db90a459541f0.
Report an issue: GitHub.
Appendix: source
Thrown at packages/adapter-utils/src/git-workspace-sync.ts:702
forceFullBundle?: boolean;
}): string {
const remoteDir = shellQuote(input.remoteDir);
const bundlePath = shellQuote(input.bundlePath);
const exportRef = shellQuote(input.exportRef);
const baseSha = shellQuote(input.baseSha);
const statusPath = input.statusPath ? shellQuote(input.statusPath) : null;
const cleanupParts = [
`rm -f ${bundlePath}`,
...(statusPath ? [`rm -f ${statusPath}`] : []),
`git -C ${remoteDir} update-ref -d ${exportRef} >/dev/null 2>&1 || true`,
];
return [
"set -e",
input.cleanupBundle ? `cleanup() { ${cleanupParts.join("; ")}; }` : "",
input.cleanupBundle ? "trap cleanup EXIT" : "",
`mkdir -p ${shellQuote(path.posix.dirname(input.bundlePath))}`,
`rm -f ${bundlePath}`,
// Choose the bundle boundary. A thin bundle `HEAD --not <baseSha>` records
// baseSha as a prerequisite the importer (host) must already hold. That
// assumption breaks in two real cases, and then `git fetch` on the host
// hard-fails with "did not send all necessary objects" and the run's work
// is lost:
// 1. The sandbox HEAD has diverged from baseSha (e.g. a local-only branch
// that forked from an older commit) — the host may still hold baseSha,
// but a repo whose history is inconsistent cannot satisfy the walk.
// 2. The host workspace no longer holds baseSha at import time (a shared
// workspace that was reset/re-realized between export and import).
// Bundle relative to the merge-base of baseSha and HEAD instead: that
// merge-base is an ancestor of baseSha, so any host that holds baseSha (or
// an ancestor of it) can satisfy the prerequisite, while the bundle stays a
// delta. When baseSha is absent from the sandbox — or no merge-base exists,
// or the caller forces it after a delta import failed on a missing
// prerequisite — fall back to a full, self-contained bundle with no
// prerequisites.
...(input.forceFullBundle
? [`bundle_base=""`]View on GitHub (pinned to 3f1d897a7c)