Skip to content

Release cut for the Aug adopt/webui hardening + 2 residual staging gaps #288

Description

Two asks: a release cut, and two residual staging gaps the August hardening left open.

Ask 1 — cut a release

PyPI skillopt latest is 0.2.0, released 2026-07-02, with no release since (checked today; pyproject.toml at main still says 0.2.0). Since that wheel was built, main merged:

So pip install skillopt today installs an adopt() with none of #241's checks and a webui that binds 0.0.0.0 by default — while the fixes have been merged for a month. #117 (closed) already showed the practical cost of this exact lag: a wheel-content bug fixed at HEAD that kept biting every fresh install. A 0.2.1 / 0.3.0 cut would make the installable artifact match the hardened tree.

Ask 2 — two residual gaps at HEAD (main @ 79124b37, skillopt_sleep/staging.py blob 615c5ac8cf4c70ccb93e0283e8f7b1439890b6e2)

(a) the legacy SKILL/CLAUDE pair adopt still has no roots containment

_live_target_within_roots (L368 — its own docstring: "the manifest is not a trust boundary") is called on exactly one path: the per-skill adoption (L1508). The legacy-pair block at the bottom of the file (L~3058-3130) validates pins, symlinks/hardlinks, and realpath identity — but never checks the live target against the roots recorded when the night was staged, and _planned_live_directories (L3069) will create parent directories for an absent target at any absolute *.md path the manifest names. The per-skill path gained exactly this containment in #241; the legacy pair didn't. Suggested: re-derive legacy live targets from current config the way the per-skill path does, and run the same _live_target_within_roots check there.

(b) staging has no provenance marker — a staging tree that arrives with a cloned repo is indistinguishable from engine output

Staging lives at <project>/.skillopt-sleep/staging (L829) — project-relative. Nothing about a staging night records that the local engine produced it: the manifest format, file layout, and sha256 pins are all self-consistent and documented, and adopt verifies self-consistency only. #241's threat model (from the PR body) was locally generated, then tampered — "it needs local write access to the staging manifest". The repo-shipped vector needs no local tampering: a cloned project can carry a ready-made .skillopt-sleep/staging/<night>/ and a README/CONTRIBUTING line like "run skillopt-sleep adopt to apply our pre-staged tuned skills". On the current 0.2.0 wheel that is an arbitrary-destination write of repo-authored bytes (verified below); at HEAD the per-skill path confines it to the skills roots, but the legacy pair (gap a) does not.

Suggested: a provenance marker the repo cannot forge — a state-side registry of staged nights (outside the project, e.g. under the sleep state dir), or an HMAC of the manifest keyed by a sleep-state secret — so adopt rejects staging that did not originate from the local engine.

Reproduction on the current wheel (0.2.0)

A scratch clone containing only .skillopt-sleep/staging/20260920-120000/{manifest.json, proposed_SKILL.md, proposed_CLAUDE.md}, with the manifest's live_skill_path set to ../../../../../staged-outside-repo.md, then the product's own documented apply command in that directory:

$ skillopt-sleep adopt
[sleep] adopted from <clone>\.skillopt-sleep\staging\20260920-120000
   -> ../../../../../staged-outside-repo.md
   -> ./CLAUDE.md

Result: the repo-authored bytes land at the drive root (the overlong ../ chain clamps there on Windows — destination independent of clone depth and username), plus the clone's own CLAUDE.md is overwritten. With an absolute live_skill_path under <home>\.claude\skills\<name>\SKILL.md, the bytes land in the user-global skills root that Claude Code auto-loads in every future session, every project. The wheel's adopt() core (staging.py L137-161 in 0.2.0) is makedirs + shutil.copy2 on the manifest value — no path safety, no pins, no authorship check. At HEAD these specific writes are contained by #241 on the per-skill path (which is why the release cut matters first); the residual is gaps (a) and (b) above.

Happy to help with either a release checklist or a PR for the legacy containment + provenance marker.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions