Skip to content

feat: lock=ignore, none-as-footgun-guard, fix integrity story & bundle scope check#16

Merged
ChALkeR merged 2 commits into
masterfrom
claude/review-src-code-hcn55
May 16, 2026
Merged

feat: lock=ignore, none-as-footgun-guard, fix integrity story & bundle scope check#16
ChALkeR merged 2 commits into
masterfrom
claude/review-src-code-hcn55

Conversation

@exo-nikita
Copy link
Copy Markdown
Collaborator

Three concerns from the high-level audit, addressed together because they all touch the constructor's per-dir block in state.js and the Config getters.

1. Drop the tautological hash check (lock=none + bundle=load)

The previous code populated this.hashes from sha512(bundle source) and
then asserted that the same hash matched sha512(source) in getFile
zero protection, while reading like an integrity check.

The intent for lock=none + bundle=load is correct: the bundle is the
source of truth, and shipping hashes-of-the-same-sources in the same file
is redundant. Express that directly: stop populating hashes from the
bundle, and skip the hash assertion in getFile when !useLockfile.
lock=frozen + bundle=load still verifies (lockfile's hashes vs bundle's
source).

2. Verify bundle scope on load

The lockfile loader does if (frozen) assert.equal(json.config.scope, this.config.scope). Mirror that for bundle loading — a bundle saved with scope=full loaded under scope=node_modules (or vice versa) is now rejected in frozen mode.

3. lock=ignore mode; none becomes a footgun guard

Today lock=none silently skips an existing stasis.lock.json, and bundle=ignore behaves the same as bundle=none (both throw on file presence). Repurpose none as the implicit default that refuses to silently ignore an existing lockfile/bundle, and add ignore as the explicit "I know, leave it alone" mode for both lock and bundle:

mode \ on disk file present file absent
lock=none (default) reject (NEW) OK
lock=ignore (NEW) tolerate, don't load, don't write OK
bundle=none (default) reject (unchanged) OK
bundle=ignore tolerate (FIX — previously rejected like none) OK

useLockfile now also excludes ignore (parallel to bundle). bundle=load now allows lock=ignore alongside lock=frozen / lock=none. CLI banner adds ignore to --lock and updates the bundle=load error message. =none is intentionally not in the help — it's the implicit default, and surfacing it would defeat the footgun guard.

Tests (104/104 passing)

  • Config: lock=ignore getters; bundle=load works with lock=ignore.
  • CLI:
    • lock=none rejects an existing lockfile (explicit and implicit-default forms).
    • lock=ignore tolerates an existing lockfile and doesn't touch it.
    • lock=ignore + bundle=load serves from the bundle without lockfile interaction.
    • bundle=none rejects an existing bundle file.
    • bundle=ignore tolerates an existing bundle and leaves it untouched (regression for the previous "ignore behaves like none" bug).
    • lock=frozen + bundle=load rejects a bundle with a mismatching scope.
    • --bundle=load --lock=replace is rejected (added an explicit test alongside the existing lock=add one).

Test plan

  • pnpm test — 104/104 pass
  • pnpm lint — clean
  • Manual end-to-end: lock=frozen+bundle=load blocks tampered bundle with ERR_ASSERTION; lock=ignore+bundle=load runs the tampered payload (intentional, documented); lock=none with an existing lockfile fails with Unexpected …/stasis.lock.json with config.lock = 'none'.

Generated by Claude Code

claude added 2 commits May 16, 2026 04:35
…ty story

Three concerns from the high-level audit, addressed together because they
all touch the same code paths:

1. Drop the tautological hash check (lock=none + bundle=load)

The previous behaviour populated this.hashes from sha512(bundle source) and
then asserted that hash matched sha512(bundle source) at getFile time --
zero protection, while reading like an integrity check. The intent for
lock=none + bundle=load is that the bundle is the source of truth and the
hashes-in-lockfile would be redundant when source-in-bundle is right next
to them. Express that directly: stop populating hashes from the bundle,
and skip the hash assertion in getFile when !useLockfile. lock=frozen +
bundle=load still verifies (the lockfile's hashes vs the bundle's source).

2. Verify bundle scope on load (parallel to the lockfile check)

In frozen mode, the lockfile loader does assert.equal(json.config.scope,
this.config.scope). Mirror that for bundle loading -- a bundle saved with
scope=full loaded under scope=node_modules (or vice versa) is now rejected
in frozen mode with ERR_ASSERTION.

3. lock=none becomes a footgun guard; add lock=ignore as the explicit
   opt-in to tolerate file presence

Today lock=none silently skips an existing stasis.lock.json, and
bundle=ignore behaves the same as bundle=none (both throw when the file is
present). Repurpose `none` as the implicit default that refuses to silently
ignore an existing lockfile/bundle, and add `ignore` as the explicit "I
know, leave it alone" mode for both lock and bundle:

- lock=none + stasis.lock.json on disk           -> reject (NEW)
- lock=ignore + stasis.lock.json on disk         -> tolerate, don't load,
                                                    don't write (NEW mode)
- bundle=none + stasis.code.br on disk           -> reject (unchanged)
- bundle=ignore + stasis.code.br on disk         -> tolerate (FIX -- used
                                                    to reject identically
                                                    to bundle=none)

useLockfile now also excludes `ignore` (parallel to bundle). bundle=load
now allows lock=ignore alongside lock=frozen / lock=none. CLI usage banner
adds `ignore` to --lock and updates the bundle=load error message.
=none is intentionally not in the help text: it's the implicit default,
and surfacing it would defeat the footgun guard.

Tests:
- Config: lock=ignore getters, bundle=load works with lock=ignore.
- CLI:
  - lock=none rejects an existing lockfile (explicit and implicit-default).
  - lock=ignore tolerates an existing lockfile, doesn't touch it.
  - lock=ignore + bundle=load serves from bundle without lockfile.
  - bundle=none rejects an existing bundle file.
  - bundle=ignore tolerates and leaves it untouched (regression for the
    previous "ignore behaves like none" bug).
  - lock=frozen + bundle=load rejects a bundle with a mismatching scope.
  - bundle=load + lock=replace is rejected (added an explicit test
    alongside the existing lock=add one).

Docs updated, kept compact.
Per review: the bundle's structure does not vary by scope (sources/formats/
imports are unconditional), so a scope mismatch is a configuration error
in any mode, not only under lock=frozen. Drop the frozen guard and assert
scope equality whenever the bundle is loaded. Add a non-frozen regression
test (lock=add + bundle=add) to lock the stricter behaviour.
@ChALkeR ChALkeR merged commit 10d3964 into master May 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants