sealcode 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,12 +84,14 @@ sealcode lock # encrypt source → vendor/ blobs (removes plaintext)
84
84
  sealcode unlock # decrypt blobs → restore source (removes stubs)
85
85
  sealcode verify # confirm every blob decrypts & matches its hash
86
86
  sealcode status # show locked/unlocked + drift since last lock
87
- sealcode status --check # exit 1 if unlocked with drift (used by git hook)
87
+ sealcode status --check # exit 1 if unlocked (used by git hook). 1.4.1+: strict — passes only when locked.
88
+ sealcode status --check --allow-clean-unlock # 1.4.1+ escape hatch: only fail on drift
88
89
  sealcode status --json # machine-readable state (editors / scripts)
89
90
  sealcode rotate # change passphrase (blobs unchanged); env: SEALCODE_OLD_PASSPHRASE / SEALCODE_NEW_PASSPHRASE
90
91
  sealcode backup <dir> # copy locked vault + config snapshot to a new folder
91
92
  sealcode restore <dir> # restore from backup (use --force if locked dir exists)
92
- sealcode install-hook # git pre-commit: block commits when unlocked + drift
93
+ sealcode install-hook # git pre-commit: block any commit while the project is unlocked (strict by default in 1.4.1+)
94
+ sealcode install-hook --lenient # pre-1.4.1 behavior: only block when the working tree has drifted vs the lock
93
95
  sealcode uninstall-hook # remove hook block
94
96
  sealcode panic # immediate re-lock + session wipe
95
97
  sealcode logout # clear cached session, force passphrase next time
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sealcode",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Lock your source code in your own git repo. Stop AI agents, scrapers, and curious eyes from reading what's yours.",
5
5
  "keywords": [
6
6
  "encryption",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "https://github.com/sealcode/sealcode.git"
27
+ "url": "git+https://github.com/sealcode/sealcode.git"
28
28
  },
29
29
  "license": "MIT",
30
30
  "author": "sealcode",
package/src/cli.js CHANGED
@@ -465,14 +465,25 @@ function build() {
465
465
  program
466
466
  .command('status')
467
467
  .description('Show whether the project is locked / unlocked and any drift.')
468
- .option('--check', 'exit 1 if unlocked with drift (for git hooks)', false)
468
+ .option('--check', 'exit 1 if unlocked (for git hooks). sealcode@1.4.1: strict by default — passes only when locked.', false)
469
+ // sealcode@1.4.1 — opt back into the pre-1.4.1 "block only on
470
+ // drift" behavior. Useful for CI jobs that intentionally commit
471
+ // alongside an unlocked working tree (rare). The installed hook
472
+ // can also flip into this mode globally via
473
+ // `sealcode install-hook --lenient`.
474
+ .option('--allow-clean-unlock', 'pre-1.4.1 behavior: pass when unlocked as long as there is no drift', false)
469
475
  .option('--json', 'machine-readable output (for editors / CI)', false)
470
476
  .action(async (opts) => {
471
477
  try {
472
478
  const projectRoot = resolveProject(program.opts());
473
479
  const config = getActiveConfig(projectRoot);
474
480
  if (opts.check) {
475
- const r = await runPrecommitCheck(projectRoot, config, () => getKeyForCheck(projectRoot, config));
481
+ const r = await runPrecommitCheck(
482
+ projectRoot,
483
+ config,
484
+ () => getKeyForCheck(projectRoot, config),
485
+ { allowCleanUnlock: !!opts.allowCleanUnlock },
486
+ );
476
487
  if (!r.ok) {
477
488
  process.stderr.write(r.message + '\n');
478
489
  process.exitCode = 1;
@@ -730,12 +741,23 @@ function build() {
730
741
  // -------- install-hook --------
731
742
  program
732
743
  .command('install-hook')
733
- .description('Install a git pre-commit hook that runs `sealcode status --check`')
734
- .action(() => {
744
+ .description('Install a git pre-commit hook that runs `sealcode status --check`.')
745
+ // sealcode@1.4.1 — the hook is strict by default (blocks any
746
+ // commit while the project is unlocked). `--lenient` writes a
747
+ // hook that uses `--allow-clean-unlock` instead, restoring the
748
+ // pre-1.4.1 "block only on drift" behavior.
749
+ .option('--lenient', 'install the pre-1.4.1 hook (only blocks commits when working tree drifts)', false)
750
+ .action((opts) => {
735
751
  try {
736
752
  const projectRoot = resolveProject(program.opts());
737
- const hookPath = installHook(projectRoot);
738
- process.stdout.write(`✓ pre-commit hook installed:\n ${hookPath}\n`);
753
+ const hookPath = installHook(projectRoot, { lenient: !!opts.lenient });
754
+ process.stdout.write(`✓ pre-commit hook installed (${opts.lenient ? 'lenient' : 'strict'} mode):\n ${hookPath}\n`);
755
+ if (!opts.lenient) {
756
+ process.stdout.write(
757
+ ' Any `git commit` while the project is unlocked will be blocked.\n' +
758
+ ' Run `sealcode lock` before committing.\n',
759
+ );
760
+ }
739
761
  } catch (err) {
740
762
  process.exitCode = reportError(err);
741
763
  }
package/src/hooks.js CHANGED
@@ -41,8 +41,18 @@ function findGitDir(startDir) {
41
41
  /**
42
42
  * Install the pre-commit hook. Idempotent: replaces only the sealcode block
43
43
  * (and any legacy vaultline block left over from an older install).
44
+ *
45
+ * sealcode@1.4.1 — `opts.lenient` toggles the behavior of the hook
46
+ * itself, not just the install message. Strict (default) refuses any
47
+ * commit while the project is unlocked. Lenient appends
48
+ * `--allow-clean-unlock`, restoring the pre-1.4.1 "block only on drift"
49
+ * behavior for niche workflows that intentionally commit alongside an
50
+ * unlocked tree.
51
+ *
52
+ * @param {string} projectRoot
53
+ * @param {{ lenient?: boolean }} [opts]
44
54
  */
45
- function installHook(projectRoot) {
55
+ function installHook(projectRoot, opts = {}) {
46
56
  const gitDir = findGitDir(projectRoot);
47
57
  if (!gitDir) {
48
58
  throw new Error('No .git directory found above this folder. Run `git init` first.');
@@ -50,15 +60,17 @@ function installHook(projectRoot) {
50
60
  const hookPath = path.join(gitDir, 'hooks', 'pre-commit');
51
61
  ensureHooksDir(gitDir);
52
62
 
63
+ const flag = opts.lenient ? ' --allow-clean-unlock' : '';
53
64
  const block = [
54
65
  MARK_BEGIN,
66
+ `# mode: ${opts.lenient ? 'lenient' : 'strict'}`,
55
67
  'sealcode_check() {',
56
68
  ' ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0',
57
69
  ' cd "$ROOT" || return 0',
58
70
  ' if command -v sealcode >/dev/null 2>&1; then',
59
- ' sealcode status --check || exit 1',
71
+ ` sealcode status --check${flag} || exit 1`,
60
72
  ' elif command -v npx >/dev/null 2>&1; then',
61
- ' npx --yes sealcode@latest status --check || exit 1',
73
+ ` npx --yes sealcode@latest status --check${flag} || exit 1`,
62
74
  ' else',
63
75
  ' echo "sealcode: install the CLI (npm i -g sealcode) or npx for pre-commit checks." >&2',
64
76
  ' exit 1',
package/src/status.js CHANGED
@@ -155,11 +155,27 @@ function renderStatus(status) {
155
155
  }
156
156
 
157
157
  /**
158
- * Pre-commit / CI gate: pass only if locked, or unlocked with no drift.
159
- * Caller supplies `getK` — session load, env passphrase unwrap, or null.
158
+ * Pre-commit / CI gate.
159
+ *
160
+ * sealcode@1.4.1 — strict by default. Any commit while the project is
161
+ * in the `unlocked` state is rejected, regardless of whether the working
162
+ * tree has drifted vs. the last lock. Before 1.4.1 we only blocked when
163
+ * drift was non-zero, which let a recipient `sealcode unlock && git add
164
+ * . && git commit` push the entire decrypted source into git without a
165
+ * single warning. The new default closes that footgun.
166
+ *
167
+ * Callers (CI scripts, IDE integrations, niche workflows) can opt back
168
+ * into the old "block only on drift" behavior with
169
+ * `{ allowCleanUnlock: true }`. The flag is also surfaced as a CLI
170
+ * option on `sealcode status --check`.
171
+ *
172
+ * @param {string} projectRoot
173
+ * @param {object} config
160
174
  * @param {( ) => Promise<Buffer|null>} getK
175
+ * @param {{ allowCleanUnlock?: boolean }} [opts]
161
176
  */
162
- async function runPrecommitCheck(projectRoot, config, getK) {
177
+ async function runPrecommitCheck(projectRoot, config, getK, opts = {}) {
178
+ const allowCleanUnlock = !!opts.allowCleanUnlock;
163
179
  const s = await runStatus({ projectRoot, config });
164
180
  if (!s.initialized) return { ok: true, message: '' };
165
181
  if (s.state === 'locked') return { ok: true, message: '' };
@@ -171,6 +187,21 @@ async function runPrecommitCheck(projectRoot, config, getK) {
171
187
  };
172
188
  }
173
189
 
190
+ // Strict default: refuse to let an unlocked tree reach `git commit`.
191
+ // We do this BEFORE computing drift because computing drift requires
192
+ // K, and we don't want a failed `getK()` to mask a much more
193
+ // important "you are about to commit plaintext source" warning.
194
+ if (!allowCleanUnlock) {
195
+ return {
196
+ ok: false,
197
+ message:
198
+ 'sealcode: project is UNLOCKED. Committing now would put plaintext source into git.\n' +
199
+ ' Fix: run `sealcode lock` first, then `git add` the updated locked blobs.\n' +
200
+ ' (Override only if you really mean it: `sealcode status --check --allow-clean-unlock`,\n' +
201
+ ' or re-install the hook with `sealcode install-hook --lenient`.)',
202
+ };
203
+ }
204
+
174
205
  const K = await getK();
175
206
  if (!K) {
176
207
  return {