portable-resume-skills is a local, offline-friendly tool that:
- Reads untrusted persisted agent session stores from disk (JSONL, SQLite, etc.).
- Emits partially sanitized, inert-marked handoff text for a fresh session.
- Can install skill packages into project or user skill roots (those skills are then loadable by host agents).
It is not a network service and does not restore live processes.
- Local session history (may contain secrets, private paths, credentials).
- Destination skill roots (project/global agent skill directories).
- Source session stores (must remain byte/mtime unchanged).
- Temporary private SQLite/file copies created during stable reads.
| Boundary | Threat | Control |
|---|---|---|
| Source store → reader | Path escape, symlink, special files, concurrent mutation | Approved roots, no-follow regular files, stable snapshot, fail closed |
| request-v1 file → reader | Pathname swap / symlink race on --request-file |
Pre-open lstat identity bound to opened fd; final-component dir_fd + O_NOFOLLOW; double-read content verify; fail closed without symlink-safe open primitives |
| Recovered text → destination model | Prompt injection | inert/untrusted markers, sanitizer, quoted handoff, re-check checklist |
| Installer → skill roots | Overwrite non-owned files, path escape, parent/control symlink races, malicious skill supply chain | Ownership/claims, refuse collisions, pinned root lock, no-follow control store (.portable-resume lock/journal/manifest atomic replace), descriptor-relative payload commit/rollback/uninstall/verify, sandboxed journal stage/backup cleanup, dry-run |
| Optional zstd | Process boundary | Trusted absolute decoder paths only, fixed argv, shell=False, empty PATH, caps/timeouts |
Optional portable-resume/request-v1 documents are read only as regular, non-symlink files bounded by DEFAULT_BOUNDS.request_bytes. On POSIX platforms with O_NOFOLLOW / O_DIRECTORY / dir_fd:
- Pre-open
lstatrequires a small regular file and records(st_dev, st_ino, mode, size, mtime_ns). - The basename is opened descriptor-relative under the parent with
O_NOFOLLOW(intermediate parent-path aliases such as macOS/varmay still resolve). - The first
fstatmust match the pre-open fingerprint so a concurrent regular→regular pathname replacement cannot be accepted as the originally validated inode. - Bytes are read twice from the same descriptor; metadata must remain stable through a final pathname
lstat.
Platforms that cannot guarantee symlink-safe final-component open (including Windows without these primitives) fail closed for request-file mode rather than claiming no-follow protection from a raceable pathname check alone. Mutation or replacement fails deterministically with content-free E_INVALID_INPUT diagnostics — request bytes from a failed attempt never reach JSON parsing.
Installing skills means writing instruction files that a host agent may later execute with the user’s privileges.
- Treat any third-party revision as untrusted until reviewed.
- Prefer
--scope projectand--dry-runfirst. --scope globalwrites under the host’s user skill roots (seedocs/host-support.md).--force-with-backupmay replace non-owned conflicting files after copying them under.portable-resume/backups/.- If a host ignores skill instructions and shells user text, that is out of scope for the fixed runner argv (which never includes user fields).
Redaction is best-effort, not complete DLP. The tracked-tree secret gate (scripts/check_secrets.py) scans git ls-files only (not git history). Runtime patterns include common shapes such as PEM private-key blocks, Slack xox*, Google-like AIza…, and:
- AWS-like
AKIA… - Common
sk-…/ Bearer-like tokens - Simple
label=valuesecret-looking pairs - Some metadata field names (
system_prompt, signatures, etc.)
Not guaranteed: arbitrary JWTs, uncommon cloud tokens, free-form passwords, secrets split across turns or obfuscated. PEM / Slack / AIza shapes are best-effort covered above but residual secrets may still remain in handoff text and host logs.
On POSIX (Linux/macOS) the installer:
- Pins the skill root and rejects a symlinked or non-directory
.portable-resumesupport dir. - Opens
install.lock,journal.json, andmanifest.jsonwith no-follow regular-file checks; lock metadata is truncated before rewrite. - Writes journal and manifest via unique temporary names under the support dir, then atomic replace (not a predictable
*.tmppathname open). - Commits, rolls back, uninstalls, and verifies payload files through directory file descriptors (
O_NOFOLLOW) so a parent directory swapped for a symlink after planning cannot redirect writes/deletes outside the skill root.
Windows remains weaker: exclusive locking and full dirfd semantics are residual under issue #29; mutating install/uninstall fails closed where descriptor-relative primitives are unavailable rather than falling back to unsafe ambient pathname replace.
- Recovered text remains model-visible; operators must not treat handoff as instructions.
- Temp snapshot directories may briefly hold session-derived bytes; failures should clean up, but crash windows exist.
- Handoff on stdout may be captured by host logging.
- Windows file locking is weaker than POSIX flock in V1; installer path races on Windows are gated fail-closed (#29) rather than claimed parity.
- The only intentional child process is the optional trusted zstd decoder for compressed Codex rollouts.
- Previous-manifest byte snapshots inside the journal for crash forensics remain best-effort via per-path rollback files; a richer previous-manifest generation record may still land under follow-up installer work.
Security fixes target the main branch of this repository.
- Prefer GitHub Security Advisories when enabled for this repository.
- Otherwise contact the repository owner via their GitHub security contact.
- Include: impact, reproduction steps, affected commit SHA, and whether session data was involved.
Do not attach real private transcripts or credentials to public issues.
- Prefer project-scope installs when evaluating unknown revisions.
- Avoid
--force-with-backupunless you understand which non-owned paths will be replaced. - Treat handoff output as stale evidence; re-verify repository state before acting.
- Never point
--source-rootat trees you do not intend to parse. - Do not paste handoff blobs into public tickets without redaction review.