pqcheck 0.16.15 → 0.16.16

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
@@ -8,7 +8,7 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dm/pqcheck.svg?style=flat-square&color=06b6d4)](https://www.npmjs.com/package/pqcheck)
9
9
  [![license](https://img.shields.io/npm/l/pqcheck.svg?style=flat-square&color=06b6d4)](./LICENSE)
10
10
 
11
- > **Latest: v0.16.12** — ⚠️ **Existing GitHub Action users:** one-line fix required in `.github/workflows/cipherwake.yml` (`uses: cipherwakelabs/pqcheck@v3` → `cipherwakelabs/pqcheck/action@v3`). The old ref was broken since v0.15 and silently failed every CI run; today's end-to-end test caught it. Re-running `pqcheck onboard` also regenerates the workflow correctly. Plus: README rewritten to advertise both `pqcheck <domain>` and `pqcheck deploy-check --ai` equally, rate limits corrected. [Full changelog →](./CHANGELOG.md)
11
+ > **Latest: v0.16.16** — `pqcheck setup` now defaults to per-project install of the Claude Code statusLine + hooks (`./.claude/settings.json` instead of `~/.claude/settings.json`). The Cipherwake badge will only fire in projects where you ran setup, not across every Claude Code session on the machine. Pass `--scope global` to opt back into machine-wide install. [Full changelog →](./CHANGELOG.md)
12
12
 
13
13
  ## Two ways to use it
14
14
 
package/bin/pqcheck.js CHANGED
@@ -4441,7 +4441,7 @@ function renderReleaseChecklist(domain, opts = {}) {
4441
4441
  // `pqcheck init` — interactive workflow scaffold (habit-loop #4, locked 2026-05-16)
4442
4442
  // =============================================================================
4443
4443
  // Writes a ready-to-commit .github/workflows/cipherwake.yml that calls
4444
- // cipherwakelabs/pqcheck/action@v3 in trust-diff mode. Zero copy-paste docs friction.
4444
+ // cipherwakelabs/pqcheck@v4 in trust-diff mode. Zero copy-paste docs friction.
4445
4445
  //
4446
4446
  // Flags:
4447
4447
  // --domain <d> Skip the domain prompt
@@ -4618,7 +4618,7 @@ jobs:
4618
4618
  runs-on: ubuntu-latest
4619
4619
  steps:
4620
4620
  - name: Run Cipherwake Trust Diff
4621
- uses: cipherwakelabs/pqcheck/action@v3
4621
+ uses: cipherwakelabs/pqcheck@v4
4622
4622
  with:
4623
4623
  mode: trust-diff
4624
4624
  domain: ${domain}
@@ -5613,9 +5613,23 @@ async function runProtocolCommand(args) {
5613
5613
  console.log("Here's what would be added:");
5614
5614
  console.log("");
5615
5615
  if (detected.length === 0) {
5616
+ // Default to creating project-local ./CLAUDE.md rather than global
5617
+ // ~/.claude/CLAUDE.md. Mirrors the cli/setup --scope=project default
5618
+ // (commit 4450e77): don't pollute global config when no signal exists
5619
+ // that the user wants machine-wide installation. Pass --scope=global
5620
+ // to override and create ~/.claude/CLAUDE.md instead.
5621
+ const scopeRaw = (parseFlag(args, "--scope") || "project").toLowerCase();
5622
+ const useGlobal = scopeRaw === "global";
5623
+ const fallbackPath = useGlobal
5624
+ ? path.join(os.homedir(), ".claude", "CLAUDE.md")
5625
+ : path.join(process.cwd(), "CLAUDE.md");
5626
+ const fallbackDisplay = fallbackPath.replace(os.homedir(), "~");
5616
5627
  console.log(color("dim", " No existing CLAUDE.md / .cursorrules / .aider.conf.yml found."));
5617
- console.log(color("dim", " Creating ~/.claude/CLAUDE.md with the protocol."));
5618
- detected.push({ label: "Claude Code (will create)", path: path.join(os.homedir(), ".claude", "CLAUDE.md") });
5628
+ console.log(color("dim", ` Creating ${fallbackDisplay} with the protocol.`));
5629
+ if (!useGlobal) {
5630
+ console.log(color("dim", " (pass --scope global to create ~/.claude/CLAUDE.md instead)"));
5631
+ }
5632
+ detected.push({ label: useGlobal ? "Claude Code (will create global)" : "Claude Code (will create project)", path: fallbackPath });
5619
5633
  }
5620
5634
  for (const d of detected) {
5621
5635
  console.log(` • Append a ~30-line "## Pre-deploy verification with Cipherwake" section to ${color("bold", d.path)}`);
@@ -5927,11 +5941,21 @@ async function runSetupCommand(args) {
5927
5941
  const skipHook = args.includes("--skip-hook");
5928
5942
  const skipStatusline = args.includes("--skip-statusline");
5929
5943
  const skipVscode = args.includes("--skip-vscode");
5944
+ // Where to install Claude Code statusLine + hooks. Default 'project' keeps
5945
+ // Cipherwake scoped to the current repo (avoids the "Cipherwake badge
5946
+ // follows me into unrelated projects" UX bug). 'global' is opt-in for
5947
+ // power users who want one canonical domain badge across every project.
5948
+ const settingsScope = (parseFlag(args, "--scope") || "project").toLowerCase();
5949
+ if (!["project", "global"].includes(settingsScope)) {
5950
+ console.error(color("red", `error: --scope must be 'project' or 'global' (got '${settingsScope}')`));
5951
+ process.exit(3);
5952
+ }
5930
5953
 
5931
5954
  if (!domain) {
5932
5955
  console.error(color("red", "error: pqcheck setup requires --domain"));
5933
5956
  console.error(color("dim", "Usage: npx pqcheck setup --auto --domain example.com"));
5934
- console.error(color("dim", " --plan Print the install plan without writing any files"));
5957
+ console.error(color("dim", " --plan Print the install plan without writing any files"));
5958
+ console.error(color("dim", " --scope project|global Where to install Claude Code hooks (default: project — this repo only)"));
5935
5959
  console.error(color("dim", " --invoked-by=\"<name>\" --consent-phrase=\"<words>\" (audit trail)"));
5936
5960
  console.error(color("dim", "Skip flags: --skip-workflow --skip-protocol --skip-hook --skip-statusline --skip-vscode"));
5937
5961
  process.exit(3);
@@ -5977,11 +6001,17 @@ async function runSetupCommand(args) {
5977
6001
  }
5978
6002
  if (!skipHook) planEntries.push({ what: "git pre-push hook", to: path.join(process.cwd(), ".git", "hooks", "pre-push"), op: "create (if git repo)" });
5979
6003
  if (!skipStatusline) {
5980
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
6004
+ const settingsPath = settingsScope === "global"
6005
+ ? path.join(os.homedir(), ".claude", "settings.json")
6006
+ : path.join(process.cwd(), ".claude", "settings.json");
5981
6007
  let exists = false;
5982
6008
  try { await fs.access(settingsPath); exists = true; } catch { /* */ }
5983
- planEntries.push({ what: "Claude Code statusLine", to: settingsPath, op: exists ? "deep-merge (backup first)" : "create" });
6009
+ const scopeNote = settingsScope === "global"
6010
+ ? " [GLOBAL — fires in every project on this machine]"
6011
+ : " [PROJECT-LOCAL — fires only when Claude Code runs in this directory]";
6012
+ planEntries.push({ what: `Claude Code statusLine${scopeNote}`, to: settingsPath, op: exists ? "deep-merge (backup first)" : "create" });
5984
6013
  planEntries.push({ what: "Claude Code chat-hook (PostToolUse Bash)", to: settingsPath, op: exists ? "deep-merge into hooks.PostToolUse" : "create" });
6014
+ planEntries.push({ what: "Claude Code prompt-hook (UserPromptSubmit)", to: settingsPath, op: exists ? "deep-merge into hooks.UserPromptSubmit" : "create" });
5985
6015
  }
5986
6016
  if (!skipVscode) planEntries.push({ what: "VS Code / Cursor extension", to: "via `code --install-extension cipherwakelabs.cipherwake-statusbar`", op: "attempt (soft-fail if Marketplace listing missing)" });
5987
6017
  for (const e of planEntries) {
@@ -6012,6 +6042,48 @@ async function runSetupCommand(args) {
6012
6042
  console.log("");
6013
6043
  }
6014
6044
 
6045
+ // Resolve where to write Claude Code statusLine + hook entries.
6046
+ // Default 'project' = ./.claude/settings.json (this repo only). 'global' = ~/.claude/settings.json
6047
+ // (fires in every Claude Code session on this machine). Project-local is the right default for
6048
+ // anyone with multiple projects; global is the legacy behavior and now an opt-in for the
6049
+ // "one canonical domain across everything" use case.
6050
+ const claudeSettingsPath = settingsScope === "global"
6051
+ ? path.join(os.homedir(), ".claude", "settings.json")
6052
+ : path.join(process.cwd(), ".claude", "settings.json");
6053
+
6054
+ if (!skipStatusline) {
6055
+ const displayPath = claudeSettingsPath.replace(os.homedir(), "~");
6056
+ const scopeLabel = settingsScope === "global"
6057
+ ? `global (${displayPath} — fires in EVERY project on this machine)`
6058
+ : `project (${displayPath} — fires only when Claude Code runs in this directory)`;
6059
+ console.log(color("dim", `Settings scope: ${scopeLabel}`));
6060
+ console.log(color("dim", settingsScope === "global"
6061
+ ? ` (omit --scope or pass --scope project to install for this repo only)`
6062
+ : ` (pass --scope global to install for every project on this machine)`));
6063
+ console.log("");
6064
+
6065
+ // Detect existing GLOBAL Cipherwake install while doing a project install.
6066
+ // Warn so the user doesn't end up with two layers firing on top of each other.
6067
+ if (settingsScope === "project") {
6068
+ try {
6069
+ const globalPath = path.join(os.homedir(), ".claude", "settings.json");
6070
+ const raw = await fs.readFile(globalPath, "utf8");
6071
+ const parsed = JSON.parse(raw);
6072
+ const hasGlobalCipherwake =
6073
+ (parsed.statusLine?.command && String(parsed.statusLine.command).includes("cipherwake")) ||
6074
+ JSON.stringify(parsed.hooks || {}).includes("cipherwake");
6075
+ if (hasGlobalCipherwake) {
6076
+ console.log(color("yellow", ` ⚠ A GLOBAL Cipherwake install already exists at ~/.claude/settings.json`));
6077
+ console.log(color("dim", ` It will continue firing across every project. To remove the global install,`));
6078
+ console.log(color("dim", ` edit ~/.claude/settings.json and delete the statusLine entry and the`));
6079
+ console.log(color("dim", ` cipherwake-chat-hook / cipherwake-prompt-hook commands. Backups are taken`));
6080
+ console.log(color("dim", ` automatically before any write, so changes are reversible.`));
6081
+ console.log("");
6082
+ }
6083
+ } catch { /* no global install — fine */ }
6084
+ }
6085
+ }
6086
+
6015
6087
  const installSummary = [];
6016
6088
 
6017
6089
  // -------------------------------------------------------------------------
@@ -6176,7 +6248,8 @@ async function runSetupCommand(args) {
6176
6248
  }
6177
6249
 
6178
6250
  if (!skipStatusline) {
6179
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
6251
+ const settingsPath = claudeSettingsPath;
6252
+ const displayPath = settingsPath.replace(os.homedir(), "~");
6180
6253
  try {
6181
6254
  let settings = {};
6182
6255
  let existed = false;
@@ -6187,7 +6260,7 @@ async function runSetupCommand(args) {
6187
6260
  } catch { /* will create */ }
6188
6261
  if (existed && settings.statusLine && typeof settings.statusLine === "object") {
6189
6262
  // Already has a statusLine config — don't overwrite.
6190
- console.log(color("dim", ` ⊝ ~/.claude/settings.json already has a statusLine entry — leaving alone`));
6263
+ console.log(color("dim", ` ⊝ ${displayPath} already has a statusLine entry — leaving alone`));
6191
6264
  console.log(color("dim", ` To use the Cipherwake statusline instead, set: "command": "npx --package=pqcheck@latest cipherwake-statusline"`));
6192
6265
  installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: "skipped-existing-config" });
6193
6266
  } else {
@@ -6196,7 +6269,7 @@ async function runSetupCommand(args) {
6196
6269
  settings.statusLine = { type: "command", command: "npx --package=pqcheck@latest cipherwake-statusline" };
6197
6270
  await fs.mkdir(path.dirname(settingsPath), { recursive: true });
6198
6271
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
6199
- console.log(color("green", ` ✓ added statusLine config → ~/.claude/settings.json`));
6272
+ console.log(color("green", ` ✓ added statusLine config → ${displayPath}`));
6200
6273
  installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
6201
6274
  }
6202
6275
  } catch (err) {
@@ -6212,7 +6285,8 @@ async function runSetupCommand(args) {
6212
6285
  // doesn't clobber other hooks per CLAUDE.md Rule 17.
6213
6286
  // -------------------------------------------------------------------------
6214
6287
  if (!skipStatusline) {
6215
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
6288
+ const settingsPath = claudeSettingsPath;
6289
+ const displayPath = settingsPath.replace(os.homedir(), "~");
6216
6290
  try {
6217
6291
  let settings = {};
6218
6292
  let existed = false;
@@ -6238,7 +6312,7 @@ async function runSetupCommand(args) {
6238
6312
  );
6239
6313
 
6240
6314
  if (alreadyInstalled) {
6241
- console.log(color("dim", ` ⊝ chat-hook already configured in ~/.claude/settings.json PostToolUse — skipping`));
6315
+ console.log(color("dim", ` ⊝ chat-hook already configured in ${displayPath} PostToolUse — skipping`));
6242
6316
  installSummary.push({ component: "Claude Code chat-hook", path: settingsPath, status: "skipped-already-present" });
6243
6317
  } else {
6244
6318
  const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
@@ -6246,7 +6320,7 @@ async function runSetupCommand(args) {
6246
6320
  bashEntry.hooks.push({ type: "command", command: cipherwakeHookCmd });
6247
6321
  await fs.mkdir(path.dirname(settingsPath), { recursive: true });
6248
6322
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
6249
- console.log(color("green", ` ✓ added chat-hook (PostToolUse Bash) → ~/.claude/settings.json`));
6323
+ console.log(color("green", ` ✓ added chat-hook (PostToolUse Bash) → ${displayPath}`));
6250
6324
  console.log(color("dim", ` Every \`pqcheck\` run will now push a live status message into Claude Code chat`));
6251
6325
  installSummary.push({ component: "Claude Code chat-hook", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
6252
6326
  }
@@ -6265,7 +6339,8 @@ async function runSetupCommand(args) {
6265
6339
  // / ship_decision=pass (no spam).
6266
6340
  // -------------------------------------------------------------------------
6267
6341
  if (!skipStatusline) {
6268
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
6342
+ const settingsPath = claudeSettingsPath;
6343
+ const displayPath = settingsPath.replace(os.homedir(), "~");
6269
6344
  try {
6270
6345
  let settings = {};
6271
6346
  let existed = false;
@@ -6285,7 +6360,7 @@ async function runSetupCommand(args) {
6285
6360
  );
6286
6361
 
6287
6362
  if (alreadyInstalled) {
6288
- console.log(color("dim", ` ⊝ prompt-hook already configured in ~/.claude/settings.json UserPromptSubmit — skipping`));
6363
+ console.log(color("dim", ` ⊝ prompt-hook already configured in ${displayPath} UserPromptSubmit — skipping`));
6289
6364
  installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: "skipped-already-present" });
6290
6365
  } else {
6291
6366
  const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
@@ -6293,7 +6368,7 @@ async function runSetupCommand(args) {
6293
6368
  settings.hooks.UserPromptSubmit.push({ hooks: [{ type: "command", command: cipherwakeHookCmd }] });
6294
6369
  await fs.mkdir(path.dirname(settingsPath), { recursive: true });
6295
6370
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
6296
- console.log(color("green", ` ✓ added prompt-hook (UserPromptSubmit) → ~/.claude/settings.json`));
6371
+ console.log(color("green", ` ✓ added prompt-hook (UserPromptSubmit) → ${displayPath}`));
6297
6372
  console.log(color("dim", ` Claude will see latest ship_decision in context on every prompt (when REVIEW/BLOCK)`));
6298
6373
  installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
6299
6374
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pqcheck",
3
- "version": "0.16.15",
3
+ "version": "0.16.16",
4
4
  "description": "Deploy gate for AI-coded web apps. `pqcheck deploy-check --ai` returns ship_decision=pass|review|block for Claude Code / Cursor / Copilot / Aider to gate deploys before they ship. Anonymous, no signup, free for first use.",
5
5
  "keywords": [
6
6
  "ai-coder",