pqcheck 0.15.0 → 0.15.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
@@ -27,7 +27,9 @@ The same scanner that powers [cipherwake.io](https://cipherwake.io), the browser
27
27
  | `npx pqcheck onboard <domain>` | One command: scan → scaffold the GitHub Action → capture a vendor lockfile → set a baseline → commit + push. Zero copy-paste from docs. |
28
28
  | **`npx pqcheck guard --domain <D> -- <cmd>`** 🆕 | **Deploy guard wrapper.** Wraps any deploy command. Runs `deploy-check` first; conditionally runs `<cmd>` based on `ship_decision`. Modes: `--gate-mode balanced` (default) / `advisory` / `strict`. ONE command instead of two — the strongest single artifact for AI-coder workflows because the AI never has to remember to chain check + deploy. |
29
29
  | **`npx pqcheck protocol install`** 🆕 | **Opt-in installer** for the AI Coder Protocol — appends the pre-deploy verification rule to your `CLAUDE.md` / `.cursorrules` / `.aider.conf.yml` with explicit consent (Rule 17). One upfront question (auto / manual / no). Never silent writes. |
30
- | **`npx pqcheck setup --auto --domain <D>`** 🆕 | **One-command full setup for every AI coder.** Installs (idempotently): GitHub Action workflow, AI Coder Protocol across all detected rules files (Claude / Cursor / Copilot / Aider / Windsurf / Continue / Cline / AGENTS.md) using fenced markers (`<!-- CIPHERWAKE_AI_CODER_PROTOCOL_START/END -->`), git pre-push hook, Claude Code statusLine + chat-hook (PostToolUse Bash). Skip flags available. Backups taken before any `~/.claude/settings.json` write. Audit trail at `~/.config/cipherwake/install-prefs.json`; install manifest at `~/.config/cipherwake/install-manifest.json`. |
30
+ | **`npx pqcheck setup --auto --domain <D>`** 🆕 | **One-command full setup for every AI coder.** Installs (idempotently): GitHub Action workflow, AI Coder Protocol across all detected rules files (Claude / Cursor / Copilot / Aider / Windsurf / Continue / Cline / AGENTS.md) using fenced markers (`<!-- CIPHERWAKE_AI_CODER_PROTOCOL_START/END -->`), git pre-push hook, Claude Code statusLine + 2 hooks (PostToolUse Bash + **UserPromptSubmit** ⓝ), per-repo `.cipherwake/last-status.json` for Cursor / Copilot / Continue to read as context. Skip flags available. Backups taken before any `~/.claude/settings.json` write. Audit trail at `~/.config/cipherwake/install-prefs.json`; install manifest at `~/.config/cipherwake/install-manifest.json`. |
31
+ | **`UserPromptSubmit` hook** (v0.15.1 ⓝ) | **Claude sees `ship_decision` before responding to every prompt.** When `pqcheck setup --auto` runs, it wires `cipherwake-prompt-hook` as a Claude Code UserPromptSubmit hook. On every user prompt, the hook injects `additionalContext` with the current scan's `ship_decision` IF it's `review`/`block` and the state is <24h old. Silent when state is missing, stale, or `pass`. Different timing from the PostToolUse chat-hook: this fires *before* Claude thinks (proactive), the chat-hook fires *after* a Bash command (reactive). |
32
+ | **Per-repo state file** `.cipherwake/last-status.json` (v0.15.1 ⓝ) | **Cursor / Copilot / Continue read this for workspace context.** Every `pqcheck` scan writes the same payload as the per-user file. Created by `setup --auto`; auto-added to `.gitignore` (per-developer state, not committable). Gives AI agents inside VS Code-family editors a repo-local artifact they pick up automatically when reading workspace files. |
31
33
  | **`npx pqcheck setup --plan --domain <D>`** 🆕 | **Dry-run mode.** Prints every file change `--auto` would make (target paths + operation type: create / append-markered / deep-merge / backup-first) without writing anything. Run this first when you're not sure what `--auto` will touch. |
32
34
  | **`npx pqcheck debug-network`** 🆕 | **Connectivity diagnostic.** Probes cipherwake.io API, homepage, crt.sh upstream, and the direct Vercel URL (bypassing Cloudflare). Reports HTTP status + timing per hop. Use when "scan hung" / "command not found" / corporate proxy issues come up — surfaces the actual broken hop with an actionable cause list. |
33
35
  | **`--ai` flag** (any of the above) | **AI Coder Mode** (0.15.0). Three-layer output (banner / body / structured `CIPHERWAKE_AI_GUARD_RESULT` block) tuned for Claude Code / Cursor / Aider / Zed. Includes a `ship_decision=pass\|review\|block` field your AI coworker parses to decide whether to announce the deploy, ask you, or revert. See [/methodology/ai-coder-mode](https://cipherwake.io/methodology/ai-coder-mode). |
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // =============================================================================
3
+ // cipherwake-prompt-hook — Claude Code UserPromptSubmit hook (v0.15.1 parity)
4
+ // =============================================================================
5
+ // Fires BEFORE Claude responds to every user prompt. Injects the latest scan
6
+ // state from ~/.config/cipherwake/last-scan.json into Claude's context, so
7
+ // the AI sees ship_decision proactively (e.g., when the user says "ok deploy
8
+ // this", Claude already knows the trust posture).
9
+ //
10
+ // Different timing from cipherwake-chat-hook (PostToolUse Bash) — that one
11
+ // fires AFTER a tool runs and pushes a chat message reactively. This one
12
+ // fires BEFORE the model thinks, by injecting additionalContext via the
13
+ // hookSpecificOutput shape.
14
+ //
15
+ // Silent (no injection) when:
16
+ // • state file missing
17
+ // • state file > 24h old (don't anchor on stale data)
18
+ // • ship_decision === "pass" (no need to spam the model with good news)
19
+ //
20
+ // Wire-up via `pqcheck setup --auto` writes the entry to
21
+ // ~/.claude/settings.json under `hooks.UserPromptSubmit`.
22
+ // =============================================================================
23
+
24
+ import { readFileSync } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { homedir } from "node:os";
27
+
28
+ const STATE_FILE = process.env.CIPHERWAKE_STATE_FILE
29
+ || join(homedir(), ".config", "cipherwake", "last-scan.json");
30
+
31
+ const MAX_STATE_AGE_MS = 24 * 60 * 60 * 1000; // 24h — older than this, don't inject
32
+
33
+ function readStdin() {
34
+ return new Promise((resolve) => {
35
+ let data = "";
36
+ process.stdin.setEncoding("utf8");
37
+ process.stdin.on("data", (chunk) => (data += chunk));
38
+ process.stdin.on("end", () => resolve(data));
39
+ // If no stdin (manual invocation), resolve empty immediately
40
+ if (process.stdin.isTTY) resolve("");
41
+ });
42
+ }
43
+
44
+ function silent() {
45
+ // Output nothing — Claude Code treats empty output as "no injection".
46
+ process.exit(0);
47
+ }
48
+
49
+ async function main() {
50
+ // Drain stdin (Claude Code sends a JSON event payload). We don't actually
51
+ // need the event content here — we just decide based on cached state.
52
+ try { await readStdin(); } catch { /* ignore */ }
53
+
54
+ let state;
55
+ try {
56
+ state = JSON.parse(readFileSync(STATE_FILE, "utf8"));
57
+ } catch {
58
+ silent();
59
+ return;
60
+ }
61
+
62
+ // Freshness: only inject if the last scan is recent. Anchoring Claude on
63
+ // a week-old REVIEW state would be misleading after the user has likely
64
+ // already fixed the issue.
65
+ const writtenAt = state.written_at ? new Date(state.written_at).getTime() : 0;
66
+ if (!writtenAt || Date.now() - writtenAt > MAX_STATE_AGE_MS) {
67
+ silent();
68
+ return;
69
+ }
70
+
71
+ // Only inject for non-pass states. Don't spam the model with good news on
72
+ // every prompt — Claude already trusts that no news is good news.
73
+ const shipDecision = String(state.ship_decision || "").toLowerCase();
74
+ if (shipDecision !== "review" && shipDecision !== "block") {
75
+ silent();
76
+ return;
77
+ }
78
+
79
+ const emoji = shipDecision === "block" ? "⛔" : "⚠";
80
+ const sevLabel = shipDecision === "block" ? "BLOCK" : "REVIEW";
81
+ const dbr = typeof state.score === "number" ? state.score.toFixed(1) : "?";
82
+ const grade = state.grade || "?";
83
+ const maxSev = String(state.max_severity || "").toUpperCase();
84
+ const top = state.top_issue || "—";
85
+ const ago = Math.floor((Date.now() - writtenAt) / 60000);
86
+ const ageStr = ago < 1 ? "just now" : ago < 60 ? `${ago}m ago` : `${Math.floor(ago / 60)}h ago`;
87
+
88
+ // Build the context-injection block. Brevity matters — this lands in every
89
+ // user prompt's context, so it should be a one-paragraph reminder, not a
90
+ // multi-screen status report.
91
+ const additionalContext =
92
+ `${emoji} Cipherwake — latest scan of ${state.domain || "configured domain"}: ship_decision=${shipDecision} ` +
93
+ `(${sevLabel}, DBR ${dbr} ${grade}, max ${maxSev}, last scanned ${ageStr}). ` +
94
+ `Top issue: ${top}. ` +
95
+ `Before announcing a deploy, run \`npx pqcheck deploy-check ${state.domain || "<domain>"} --ai\` to refresh. ` +
96
+ `If ship_decision is still ${shipDecision}, surface the change to the user per the AI Coder Protocol ` +
97
+ `(https://cipherwake.io/methodology/ai-coder-protocol) — don't auto-announce.`;
98
+
99
+ // Claude Code's UserPromptSubmit hook accepts hookSpecificOutput.additionalContext
100
+ // which is injected into the model's input on this turn. The hook returns
101
+ // synchronously; suppressOutput hides anything on stdout from the UI.
102
+ process.stdout.write(JSON.stringify({
103
+ hookSpecificOutput: {
104
+ hookEventName: "UserPromptSubmit",
105
+ additionalContext,
106
+ },
107
+ suppressOutput: true,
108
+ }));
109
+ }
110
+
111
+ main().catch(() => silent());
package/bin/pqcheck.js CHANGED
@@ -24,7 +24,7 @@
24
24
  })();
25
25
 
26
26
  const API_BASE = process.env.PQCHECK_API_BASE || "https://cipherwake.io";
27
- const VERSION = "0.15.0";
27
+ const VERSION = "0.15.1";
28
28
 
29
29
  // API-key support — paid tiers (Starter $29 / Growth $79 / Scale $199) get
30
30
  // per-account monthly quotas instead of the per-IP rate limit. Set via:
@@ -633,18 +633,39 @@ function formatAiFooterBlock(fields) {
633
633
  }
634
634
 
635
635
  // Persist last-scan state to ~/.config/cipherwake/last-scan.json.
636
- // Feeds the optional cipherwake-statusline script (v0.16.0) so users get
637
- // persistent ambient state in their AI coder's status line. Best-effort —
638
- // never throws (a write failure here doesn't break the scan).
636
+ // Feeds the cipherwake-statusline + cipherwake-prompt-hook + cipherwake-chat-hook
637
+ // scripts so users get persistent ambient state in their AI coder's surfaces.
638
+ //
639
+ // v0.15.1 (2026-05-22): ALSO writes a per-repo state file at
640
+ // .cipherwake/last-status.json IF that directory exists in cwd (created by
641
+ // `pqcheck setup --auto`). This gives Cursor / Copilot / Continue / Cline
642
+ // agents a read-on-demand surface inside the repo — they see the latest
643
+ // trust posture for the customer's primary domain when scanning repo state.
644
+ //
645
+ // Best-effort — never throws (a write failure doesn't break the scan).
639
646
  async function writeLastScanFile(payload) {
640
647
  try {
641
648
  const os = await import("node:os");
642
649
  const path = await import("node:path");
643
650
  const fs = await import("node:fs/promises");
644
- const dir = path.join(os.homedir(), ".config", "cipherwake");
645
- await fs.mkdir(dir, { recursive: true });
646
- const file = path.join(dir, "last-scan.json");
647
- await fs.writeFile(file, JSON.stringify({ ...payload, written_at: new Date().toISOString() }, null, 2));
651
+ const enriched = { ...payload, written_at: new Date().toISOString() };
652
+
653
+ // Per-user state file (primary, always written)
654
+ const userDir = path.join(os.homedir(), ".config", "cipherwake");
655
+ await fs.mkdir(userDir, { recursive: true });
656
+ await fs.writeFile(path.join(userDir, "last-scan.json"), JSON.stringify(enriched, null, 2));
657
+
658
+ // Per-repo state file (secondary, only if .cipherwake/ exists in cwd
659
+ // — i.e., this repo went through `pqcheck setup --auto`). Gives Cursor/
660
+ // Copilot/Continue/Cline agents a repo-local artifact they pick up
661
+ // automatically when reading workspace state.
662
+ const repoDir = path.join(process.cwd(), ".cipherwake");
663
+ try {
664
+ await fs.access(repoDir);
665
+ await fs.writeFile(path.join(repoDir, "last-status.json"), JSON.stringify(enriched, null, 2));
666
+ } catch {
667
+ // .cipherwake/ doesn't exist here — that's fine, user didn't run setup --auto in this repo
668
+ }
648
669
  } catch {
649
670
  // best-effort
650
671
  }
@@ -4991,6 +5012,96 @@ async function runSetupCommand(args) {
4991
5012
  }
4992
5013
  }
4993
5014
 
5015
+ // -------------------------------------------------------------------------
5016
+ // Component 4c: Claude Code prompt-hook (UserPromptSubmit → cipherwake-prompt-hook)
5017
+ // v0.15.1 — pinnedai-parity item. Injects ship_decision into Claude's
5018
+ // context BEFORE Claude responds to every user prompt. Different timing
5019
+ // from 4b: chat-hook fires AFTER a tool ran (reactive), prompt-hook fires
5020
+ // BEFORE Claude responds (proactive). Silent when state is missing / stale
5021
+ // / ship_decision=pass (no spam).
5022
+ // -------------------------------------------------------------------------
5023
+ if (!skipStatusline) {
5024
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
5025
+ try {
5026
+ let settings = {};
5027
+ let existed = false;
5028
+ try {
5029
+ const raw = await fs.readFile(settingsPath, "utf8");
5030
+ settings = JSON.parse(raw);
5031
+ existed = true;
5032
+ } catch { /* will create */ }
5033
+ settings.hooks = settings.hooks || {};
5034
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit || [];
5035
+
5036
+ const cipherwakeHookCmd = "npx cipherwake-prompt-hook";
5037
+ const alreadyInstalled = settings.hooks.UserPromptSubmit.some(
5038
+ (entry) => Array.isArray(entry?.hooks) && entry.hooks.some(
5039
+ (h) => h?.type === "command" && typeof h?.command === "string" && h.command.includes("cipherwake-prompt-hook"),
5040
+ ),
5041
+ );
5042
+
5043
+ if (alreadyInstalled) {
5044
+ console.log(color("dim", ` ⊝ prompt-hook already configured in ~/.claude/settings.json UserPromptSubmit — skipping`));
5045
+ installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: "skipped-already-present" });
5046
+ } else {
5047
+ const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
5048
+ if (backupPath) console.log(color("dim", ` backup: ${backupPath}`));
5049
+ settings.hooks.UserPromptSubmit.push({ hooks: [{ type: "command", command: cipherwakeHookCmd }] });
5050
+ await fs.mkdir(path.dirname(settingsPath), { recursive: true });
5051
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
5052
+ console.log(color("green", ` ✓ added prompt-hook (UserPromptSubmit) → ~/.claude/settings.json`));
5053
+ console.log(color("dim", ` Claude will see latest ship_decision in context on every prompt (when REVIEW/BLOCK)`));
5054
+ installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
5055
+ }
5056
+ } catch (err) {
5057
+ console.log(color("red", ` ✗ prompt-hook install failed: ${err.message}`));
5058
+ installSummary.push({ component: "Claude Code prompt-hook", status: "failed", error: String(err?.message || err) });
5059
+ }
5060
+ }
5061
+
5062
+ // -------------------------------------------------------------------------
5063
+ // Component 4d: Per-repo state directory (.cipherwake/) for Cursor / Copilot
5064
+ // v0.15.1 — pinnedai-parity item. Cursor/Copilot/Continue/Cline read repo
5065
+ // state for context. Creating .cipherwake/ in the repo gives them a
5066
+ // read-on-demand surface that subsequent `pqcheck` runs (via the
5067
+ // writeLastScanFile path) populate with the latest scan state. Also adds
5068
+ // .cipherwake/ to .gitignore if not already there — per-developer state,
5069
+ // not committable.
5070
+ // -------------------------------------------------------------------------
5071
+ if (!skipStatusline) {
5072
+ try {
5073
+ const repoStateDir = path.join(process.cwd(), ".cipherwake");
5074
+ await fs.mkdir(repoStateDir, { recursive: true });
5075
+ // Write an initial placeholder so the file exists immediately. Subsequent
5076
+ // scans via writeLastScanFile will overwrite with real data.
5077
+ const placeholderPath = path.join(repoStateDir, "last-status.json");
5078
+ try {
5079
+ await fs.access(placeholderPath);
5080
+ // Already exists — preserve it.
5081
+ } catch {
5082
+ await fs.writeFile(placeholderPath, JSON.stringify({
5083
+ domain,
5084
+ ship_decision: "unknown",
5085
+ note: "Initial placeholder — run `npx pqcheck deploy-check " + domain + " --ai` to populate",
5086
+ written_at: new Date().toISOString(),
5087
+ }, null, 2));
5088
+ }
5089
+ // Add .cipherwake/ to .gitignore if missing (don't commit per-developer state).
5090
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
5091
+ let gitignore = "";
5092
+ try { gitignore = await fs.readFile(gitignorePath, "utf8"); } catch { /* may not exist */ }
5093
+ if (!/^\.cipherwake\/?\s*$/m.test(gitignore)) {
5094
+ const appended = gitignore + (gitignore.endsWith("\n") || gitignore.length === 0 ? "" : "\n") + "\n# Cipherwake per-developer scan state (read-on-demand by AI coders)\n.cipherwake/\n";
5095
+ await fs.writeFile(gitignorePath, appended);
5096
+ }
5097
+ console.log(color("green", ` ✓ created .cipherwake/last-status.json (Cursor/Copilot/Continue read this for context)`));
5098
+ installSummary.push({ component: "Per-repo state file", path: placeholderPath, status: "installed" });
5099
+ } catch (err) {
5100
+ console.log(color("red", ` ✗ per-repo state install failed: ${err.message}`));
5101
+ installSummary.push({ component: "Per-repo state file", status: "failed", error: String(err?.message || err) });
5102
+ }
5103
+ }
5104
+
4994
5105
  // -------------------------------------------------------------------------
4995
5106
  // Component 5: VS Code / Cursor extension (via `code` CLI if available)
4996
5107
  // -------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,21 +1,29 @@
1
1
  {
2
2
  "name": "pqcheck",
3
- "version": "0.15.0",
4
- "description": "HTTPS posture scanner with Preview Deploy Trust Diff for PRs, Trust Diff for CI, vendor lockfile + drift alerts, cross-tenant key map, and HNDL/quantum-decryption risk scoring. Free, no signup.",
3
+ "version": "0.15.1",
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
- "post-quantum",
7
- "cryptography",
6
+ "ai-coder",
7
+ "claude-code",
8
+ "cursor",
9
+ "copilot",
10
+ "aider",
11
+ "deploy-gate",
12
+ "deploy-check",
13
+ "ai-coder-mode",
14
+ "ship-decision",
15
+ "deploy-guard",
16
+ "ci",
8
17
  "security",
9
18
  "tls",
10
19
  "ssl",
11
20
  "scanner",
21
+ "post-quantum",
12
22
  "harvest-now-decrypt-later",
13
23
  "hndl",
14
24
  "blast-radius",
15
25
  "pqc",
16
- "quantum",
17
- "crypto-audit",
18
- "crypto-inventory"
26
+ "quantum"
19
27
  ],
20
28
  "homepage": "https://cipherwake.io",
21
29
  "bugs": "https://cipherwake.io",
@@ -33,7 +41,8 @@
33
41
  "bin": {
34
42
  "pqcheck": "./bin/pqcheck.js",
35
43
  "cipherwake-statusline": "./bin/cipherwake-statusline.js",
36
- "cipherwake-chat-hook": "./bin/cipherwake-chat-hook.js"
44
+ "cipherwake-chat-hook": "./bin/cipherwake-chat-hook.js",
45
+ "cipherwake-prompt-hook": "./bin/cipherwake-prompt-hook.js"
37
46
  },
38
47
  "files": [
39
48
  "bin/",