pqcheck 0.16.14 → 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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/bin/pqcheck.js +129 -36
  3. package/package.json +1 -1
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
@@ -24,7 +24,25 @@
24
24
  })();
25
25
 
26
26
  const API_BASE = process.env.PQCHECK_API_BASE || "https://cipherwake.io";
27
- const VERSION = "0.16.14";
27
+ const VERSION = "0.16.15";
28
+
29
+ // v0.16.15 — attribution suffix. When the CLI runs inside GitHub Actions
30
+ // (GH sets GITHUB_ACTIONS=true automatically in every step) we append
31
+ // "(pqcheck-action)" to the User-Agent so the server-side classifier
32
+ // (lib/events.ts) buckets the call as `action` rather than `cli`. Lets
33
+ // the analytics dashboard split humans/CI invocations cleanly.
34
+ //
35
+ // No new data is collected — the User-Agent string was already being
36
+ // sent on every call; this is a labeling change.
37
+ //
38
+ // Opt out via PQCHECK_DISABLE_ACTION_ATTRIBUTION=1 (UA stays plain
39
+ // `pqcheck-cli/X.Y.Z` even under GitHub Actions). The env var is only
40
+ // honored when GITHUB_ACTIONS=true — it does nothing in other contexts.
41
+ const CI_ACTION_SUFFIX =
42
+ process.env.GITHUB_ACTIONS === "true" &&
43
+ process.env.PQCHECK_DISABLE_ACTION_ATTRIBUTION !== "1"
44
+ ? " (pqcheck-action)"
45
+ : "";
28
46
 
29
47
  // API-key support — paid tier (Founder Pro $19.99/mo launch pricing, locked while sub active) gets
30
48
  // per-account monthly quotas instead of the per-IP rate limit. Set via:
@@ -38,7 +56,7 @@ const QP_API_KEY = (process.env.CIPHERWAKE_API_KEY || "").trim();
38
56
  // Builds headers with optional Authorization. Use for every CLI → API call
39
57
  // so a single env-var toggle authenticates every endpoint at once.
40
58
  function apiHeaders(extra = {}) {
41
- const h = { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION}`, ...extra };
59
+ const h = { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`, ...extra };
42
60
  if (QP_API_KEY) h.authorization = `Bearer ${QP_API_KEY}`;
43
61
  return h;
44
62
  }
@@ -274,7 +292,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
274
292
  const qs = fresh ? `?domain=${encodeURIComponent(domain)}&force=1` : `?domain=${encodeURIComponent(domain)}`;
275
293
  const resp = await fetch(`${API_BASE}/api/scan${qs}`, {
276
294
  method: "GET",
277
- headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (scan)` }),
295
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (scan)` }),
278
296
  });
279
297
  if (!quiet && format === "text") process.stderr.write("\r\x1b[K");
280
298
  if (!resp.ok) {
@@ -321,7 +339,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
321
339
  if (webhookUrl) {
322
340
  fetch(webhookUrl, {
323
341
  method: "POST",
324
- headers: { "content-type": "application/json", "user-agent": `pqcheck-cli/${VERSION}` },
342
+ headers: { "content-type": "application/json", "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}` },
325
343
  body: JSON.stringify({ domain, report, source: "pqcheck-cli", at: new Date().toISOString() }),
326
344
  }).catch(() => { /* best-effort — never fail the scan on webhook delivery */ });
327
345
  }
@@ -543,7 +561,7 @@ async function runWatch({ domains, format, quiet, threshold, webhookUrl, interva
543
561
  try {
544
562
  const resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}`, {
545
563
  method: "GET",
546
- headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (watch)` }),
564
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (watch)` }),
547
565
  });
548
566
  if (!resp.ok) continue;
549
567
  const report = await resp.json();
@@ -554,7 +572,7 @@ async function runWatch({ domains, format, quiet, threshold, webhookUrl, interva
554
572
  if (changed && webhookUrl) {
555
573
  fetch(webhookUrl, {
556
574
  method: "POST",
557
- headers: { "content-type": "application/json", "user-agent": `pqcheck-cli/${VERSION}` },
575
+ headers: { "content-type": "application/json", "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}` },
558
576
  body: JSON.stringify({
559
577
  type: "score_changed",
560
578
  domain,
@@ -899,7 +917,7 @@ async function refreshVersionCacheInBackground(cachePath) {
899
917
  const controller = new AbortController();
900
918
  const timeout = setTimeout(() => controller.abort(), 2500);
901
919
  const resp = await fetch(VERSION_REGISTRY_URL, {
902
- headers: { "Accept": "application/json", "User-Agent": `pqcheck-cli/${VERSION}` },
920
+ headers: { "Accept": "application/json", "User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}` },
903
921
  signal: controller.signal,
904
922
  });
905
923
  clearTimeout(timeout);
@@ -1829,7 +1847,7 @@ async function runLockCommand(args) {
1829
1847
  try {
1830
1848
  const resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}`, {
1831
1849
  method: "GET",
1832
- headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (lock)` }),
1850
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (lock)` }),
1833
1851
  });
1834
1852
  if (!stdout) process.stderr.write("\r\x1b[K");
1835
1853
  if (!resp.ok) {
@@ -1909,7 +1927,7 @@ function buildQxmManifest(report, crypto) {
1909
1927
  return {
1910
1928
  schema: "https://cipherwake.io/schemas/qxm/v1",
1911
1929
  schemaVersion: 1,
1912
- generator: `pqcheck-cli/${VERSION}`,
1930
+ generator: `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
1913
1931
  generatedAt: report.generatedAt || new Date().toISOString(),
1914
1932
  domain: report.domain,
1915
1933
  reachable: !!report.reachable,
@@ -2152,7 +2170,7 @@ async function runDepsCommand(args) {
2152
2170
  batch.map(async (h) => {
2153
2171
  try {
2154
2172
  const r = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(h.host)}&source=cli-deps`, {
2155
- headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (deps)` }),
2173
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (deps)` }),
2156
2174
  });
2157
2175
  if (!r.ok) return { ...h, types: Array.from(h.types), scan: null, error: `${r.status}` };
2158
2176
  const body = await r.json();
@@ -2391,7 +2409,7 @@ async function fetchPageHTML(domain) {
2391
2409
  method: "GET",
2392
2410
  redirect: "follow",
2393
2411
  signal: ctrl.signal,
2394
- headers: { "User-Agent": `pqcheck-cli/${VERSION} (deps; +https://cipherwake.io)` },
2412
+ headers: { "User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (deps; +https://cipherwake.io)` },
2395
2413
  });
2396
2414
  clearTimeout(t);
2397
2415
  if (!resp.ok) return null;
@@ -2848,7 +2866,7 @@ async function runHistoryCommand(args) {
2848
2866
  let h;
2849
2867
  try {
2850
2868
  const r = await fetch(`${API_BASE}/api/history?domain=${encodeURIComponent(domain)}&days=${days}`, {
2851
- headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (history)` }),
2869
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (history)` }),
2852
2870
  });
2853
2871
  if (!r.ok) {
2854
2872
  console.error(color("red", `error: ${r.status} ${r.statusText}`));
@@ -2931,7 +2949,7 @@ async function runChangesCommand(args) {
2931
2949
  let summary;
2932
2950
  try {
2933
2951
  const r = await fetch(`${API_BASE}/api/changes-summary?domain=${encodeURIComponent(domain)}`, {
2934
- headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (changes)` }),
2952
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (changes)` }),
2935
2953
  });
2936
2954
  if (!r.ok) {
2937
2955
  console.error(color("red", `error: ${r.status} ${r.statusText}`));
@@ -3025,7 +3043,7 @@ async function runChangesCommand(args) {
3025
3043
  async function runScanBasedDeployCheck(domain, args) {
3026
3044
  const headers = {
3027
3045
  "Content-Type": "application/json",
3028
- "User-Agent": `pqcheck-cli/${VERSION}`,
3046
+ "User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
3029
3047
  };
3030
3048
  if (QP_API_KEY) headers["Authorization"] = `Bearer ${QP_API_KEY}`;
3031
3049
 
@@ -3198,7 +3216,7 @@ async function runTrustDiffCommand(args) {
3198
3216
  // anonymous per-IP rate limit path (just like /api/scan).
3199
3217
  const headers = {
3200
3218
  "Content-Type": "application/json",
3201
- "User-Agent": `pqcheck-cli/${VERSION}`,
3219
+ "User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
3202
3220
  };
3203
3221
  if (QP_API_KEY) {
3204
3222
  headers["Authorization"] = `Bearer ${QP_API_KEY}`;
@@ -3696,7 +3714,7 @@ async function runPreviewDiffCommand(args) {
3696
3714
 
3697
3715
  const headers = {
3698
3716
  "Content-Type": "application/json",
3699
- "User-Agent": `pqcheck-cli/${VERSION}`,
3717
+ "User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
3700
3718
  };
3701
3719
  if (QP_API_KEY) {
3702
3720
  headers["Authorization"] = `Bearer ${QP_API_KEY}`;
@@ -4423,7 +4441,7 @@ function renderReleaseChecklist(domain, opts = {}) {
4423
4441
  // `pqcheck init` — interactive workflow scaffold (habit-loop #4, locked 2026-05-16)
4424
4442
  // =============================================================================
4425
4443
  // Writes a ready-to-commit .github/workflows/cipherwake.yml that calls
4426
- // 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.
4427
4445
  //
4428
4446
  // Flags:
4429
4447
  // --domain <d> Skip the domain prompt
@@ -4600,7 +4618,7 @@ jobs:
4600
4618
  runs-on: ubuntu-latest
4601
4619
  steps:
4602
4620
  - name: Run Cipherwake Trust Diff
4603
- uses: cipherwakelabs/pqcheck/action@v3
4621
+ uses: cipherwakelabs/pqcheck@v4
4604
4622
  with:
4605
4623
  mode: trust-diff
4606
4624
  domain: ${domain}
@@ -4766,7 +4784,7 @@ async function fetchVendorOrigins(domain) {
4766
4784
  try {
4767
4785
  resp = await fetch(`${API_BASE}/api/deps?domain=${encodeURIComponent(domain)}`, {
4768
4786
  method: "GET",
4769
- headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (vendors)` }),
4787
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (vendors)` }),
4770
4788
  signal: ac.signal,
4771
4789
  });
4772
4790
  } catch (err) {
@@ -4814,7 +4832,7 @@ function normalizeObservedOrigin(value) {
4814
4832
  function buildVendorLockfile(domain, origins) {
4815
4833
  return {
4816
4834
  schema_version: 1,
4817
- generator: `pqcheck-cli/${VERSION}`,
4835
+ generator: `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
4818
4836
  domain,
4819
4837
  generated_at: new Date().toISOString(),
4820
4838
  approved_script_origins: origins,
@@ -4930,7 +4948,7 @@ async function runVendorsSync(domain, outPath) {
4930
4948
  resp = await fetch(`${API_BASE}/api/vendor-allowlist?domain=${encodeURIComponent(domain)}`, {
4931
4949
  method: "GET",
4932
4950
  headers: {
4933
- "user-agent": `pqcheck-cli/${VERSION} (vendors-sync)`,
4951
+ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (vendors-sync)`,
4934
4952
  "authorization": "Bearer " + QP_API_KEY,
4935
4953
  },
4936
4954
  });
@@ -5084,7 +5102,7 @@ async function runOnboardCommand(args) {
5084
5102
  try {
5085
5103
  const resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}&source=onboard`, {
5086
5104
  method: "GET",
5087
- headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (onboard)` }),
5105
+ headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (onboard)` }),
5088
5106
  });
5089
5107
  if (resp.ok) {
5090
5108
  const report = await resp.json();
@@ -5595,9 +5613,23 @@ async function runProtocolCommand(args) {
5595
5613
  console.log("Here's what would be added:");
5596
5614
  console.log("");
5597
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(), "~");
5598
5627
  console.log(color("dim", " No existing CLAUDE.md / .cursorrules / .aider.conf.yml found."));
5599
- console.log(color("dim", " Creating ~/.claude/CLAUDE.md with the protocol."));
5600
- 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 });
5601
5633
  }
5602
5634
  for (const d of detected) {
5603
5635
  console.log(` • Append a ~30-line "## Pre-deploy verification with Cipherwake" section to ${color("bold", d.path)}`);
@@ -5909,11 +5941,21 @@ async function runSetupCommand(args) {
5909
5941
  const skipHook = args.includes("--skip-hook");
5910
5942
  const skipStatusline = args.includes("--skip-statusline");
5911
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
+ }
5912
5953
 
5913
5954
  if (!domain) {
5914
5955
  console.error(color("red", "error: pqcheck setup requires --domain"));
5915
5956
  console.error(color("dim", "Usage: npx pqcheck setup --auto --domain example.com"));
5916
- 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)"));
5917
5959
  console.error(color("dim", " --invoked-by=\"<name>\" --consent-phrase=\"<words>\" (audit trail)"));
5918
5960
  console.error(color("dim", "Skip flags: --skip-workflow --skip-protocol --skip-hook --skip-statusline --skip-vscode"));
5919
5961
  process.exit(3);
@@ -5959,11 +6001,17 @@ async function runSetupCommand(args) {
5959
6001
  }
5960
6002
  if (!skipHook) planEntries.push({ what: "git pre-push hook", to: path.join(process.cwd(), ".git", "hooks", "pre-push"), op: "create (if git repo)" });
5961
6003
  if (!skipStatusline) {
5962
- 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");
5963
6007
  let exists = false;
5964
6008
  try { await fs.access(settingsPath); exists = true; } catch { /* */ }
5965
- 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" });
5966
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" });
5967
6015
  }
5968
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)" });
5969
6017
  for (const e of planEntries) {
@@ -5994,6 +6042,48 @@ async function runSetupCommand(args) {
5994
6042
  console.log("");
5995
6043
  }
5996
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
+
5997
6087
  const installSummary = [];
5998
6088
 
5999
6089
  // -------------------------------------------------------------------------
@@ -6158,7 +6248,8 @@ async function runSetupCommand(args) {
6158
6248
  }
6159
6249
 
6160
6250
  if (!skipStatusline) {
6161
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
6251
+ const settingsPath = claudeSettingsPath;
6252
+ const displayPath = settingsPath.replace(os.homedir(), "~");
6162
6253
  try {
6163
6254
  let settings = {};
6164
6255
  let existed = false;
@@ -6169,7 +6260,7 @@ async function runSetupCommand(args) {
6169
6260
  } catch { /* will create */ }
6170
6261
  if (existed && settings.statusLine && typeof settings.statusLine === "object") {
6171
6262
  // Already has a statusLine config — don't overwrite.
6172
- 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`));
6173
6264
  console.log(color("dim", ` To use the Cipherwake statusline instead, set: "command": "npx --package=pqcheck@latest cipherwake-statusline"`));
6174
6265
  installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: "skipped-existing-config" });
6175
6266
  } else {
@@ -6178,7 +6269,7 @@ async function runSetupCommand(args) {
6178
6269
  settings.statusLine = { type: "command", command: "npx --package=pqcheck@latest cipherwake-statusline" };
6179
6270
  await fs.mkdir(path.dirname(settingsPath), { recursive: true });
6180
6271
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
6181
- console.log(color("green", ` ✓ added statusLine config → ~/.claude/settings.json`));
6272
+ console.log(color("green", ` ✓ added statusLine config → ${displayPath}`));
6182
6273
  installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
6183
6274
  }
6184
6275
  } catch (err) {
@@ -6194,7 +6285,8 @@ async function runSetupCommand(args) {
6194
6285
  // doesn't clobber other hooks per CLAUDE.md Rule 17.
6195
6286
  // -------------------------------------------------------------------------
6196
6287
  if (!skipStatusline) {
6197
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
6288
+ const settingsPath = claudeSettingsPath;
6289
+ const displayPath = settingsPath.replace(os.homedir(), "~");
6198
6290
  try {
6199
6291
  let settings = {};
6200
6292
  let existed = false;
@@ -6220,7 +6312,7 @@ async function runSetupCommand(args) {
6220
6312
  );
6221
6313
 
6222
6314
  if (alreadyInstalled) {
6223
- 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`));
6224
6316
  installSummary.push({ component: "Claude Code chat-hook", path: settingsPath, status: "skipped-already-present" });
6225
6317
  } else {
6226
6318
  const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
@@ -6228,7 +6320,7 @@ async function runSetupCommand(args) {
6228
6320
  bashEntry.hooks.push({ type: "command", command: cipherwakeHookCmd });
6229
6321
  await fs.mkdir(path.dirname(settingsPath), { recursive: true });
6230
6322
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
6231
- console.log(color("green", ` ✓ added chat-hook (PostToolUse Bash) → ~/.claude/settings.json`));
6323
+ console.log(color("green", ` ✓ added chat-hook (PostToolUse Bash) → ${displayPath}`));
6232
6324
  console.log(color("dim", ` Every \`pqcheck\` run will now push a live status message into Claude Code chat`));
6233
6325
  installSummary.push({ component: "Claude Code chat-hook", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
6234
6326
  }
@@ -6247,7 +6339,8 @@ async function runSetupCommand(args) {
6247
6339
  // / ship_decision=pass (no spam).
6248
6340
  // -------------------------------------------------------------------------
6249
6341
  if (!skipStatusline) {
6250
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
6342
+ const settingsPath = claudeSettingsPath;
6343
+ const displayPath = settingsPath.replace(os.homedir(), "~");
6251
6344
  try {
6252
6345
  let settings = {};
6253
6346
  let existed = false;
@@ -6267,7 +6360,7 @@ async function runSetupCommand(args) {
6267
6360
  );
6268
6361
 
6269
6362
  if (alreadyInstalled) {
6270
- 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`));
6271
6364
  installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: "skipped-already-present" });
6272
6365
  } else {
6273
6366
  const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
@@ -6275,7 +6368,7 @@ async function runSetupCommand(args) {
6275
6368
  settings.hooks.UserPromptSubmit.push({ hooks: [{ type: "command", command: cipherwakeHookCmd }] });
6276
6369
  await fs.mkdir(path.dirname(settingsPath), { recursive: true });
6277
6370
  await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
6278
- console.log(color("green", ` ✓ added prompt-hook (UserPromptSubmit) → ~/.claude/settings.json`));
6371
+ console.log(color("green", ` ✓ added prompt-hook (UserPromptSubmit) → ${displayPath}`));
6279
6372
  console.log(color("dim", ` Claude will see latest ship_decision in context on every prompt (when REVIEW/BLOCK)`));
6280
6373
  installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
6281
6374
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pqcheck",
3
- "version": "0.16.14",
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",