@webpieces/ai-hook-rules 0.4.710 → 0.4.712

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 (59) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/detect-ai.d.ts +5 -4
  3. package/src/adapters/detect-ai.js +5 -4
  4. package/src/adapters/detect-ai.js.map +1 -1
  5. package/src/adapters/hook-core.js +9 -3
  6. package/src/adapters/hook-core.js.map +1 -1
  7. package/src/bin/codex-guard-presence.d.ts +86 -0
  8. package/src/bin/codex-guard-presence.js +150 -0
  9. package/src/bin/codex-guard-presence.js.map +1 -0
  10. package/src/bin/codex-trust.d.ts +99 -0
  11. package/src/bin/codex-trust.js +199 -0
  12. package/src/bin/codex-trust.js.map +1 -0
  13. package/src/bin/hook-registration.d.ts +200 -43
  14. package/src/bin/hook-registration.js +218 -89
  15. package/src/bin/hook-registration.js.map +1 -1
  16. package/src/bin/l0-allowlist.d.ts +0 -30
  17. package/src/bin/l0-allowlist.js +9 -67
  18. package/src/bin/l0-allowlist.js.map +1 -1
  19. package/src/bin/l0-decide.d.ts +43 -0
  20. package/src/bin/l0-decide.js +90 -0
  21. package/src/bin/l0-decide.js.map +1 -0
  22. package/src/bin/l0-ignored-tools.d.ts +38 -0
  23. package/src/bin/l0-ignored-tools.js +44 -0
  24. package/src/bin/l0-ignored-tools.js.map +1 -0
  25. package/src/bin/setup-config.d.ts +19 -0
  26. package/src/bin/setup-config.js +370 -0
  27. package/src/bin/setup-config.js.map +1 -0
  28. package/src/bin/setup.d.ts +56 -21
  29. package/src/bin/setup.js +125 -370
  30. package/src/bin/setup.js.map +1 -1
  31. package/src/bin/shim-audit-log.js +10 -1
  32. package/src/bin/shim-audit-log.js.map +1 -1
  33. package/src/bin/shim-deny-reason.js +4 -4
  34. package/src/bin/shim-deny-reason.js.map +1 -1
  35. package/src/bin/shim.d.ts +2 -0
  36. package/src/bin/shim.js +21 -1
  37. package/src/bin/shim.js.map +1 -1
  38. package/src/bin/upgrade-shim.d.ts +1 -1
  39. package/src/bin/upgrade-shim.js +12 -8
  40. package/src/bin/upgrade-shim.js.map +1 -1
  41. package/src/core/agent-event.d.ts +16 -0
  42. package/src/core/agent-event.js +17 -1
  43. package/src/core/agent-event.js.map +1 -1
  44. package/src/core/ai-type-context.d.ts +36 -0
  45. package/src/core/ai-type-context.js +45 -0
  46. package/src/core/ai-type-context.js.map +1 -0
  47. package/src/core/decision-log.js +9 -0
  48. package/src/core/decision-log.js.map +1 -1
  49. package/src/core/l0-matrix.js +14 -6
  50. package/src/core/l0-matrix.js.map +1 -1
  51. package/src/core/l0-tooling-doc.d.ts +3 -2
  52. package/src/core/l0-tooling-doc.js +22 -10
  53. package/src/core/l0-tooling-doc.js.map +1 -1
  54. package/src/core/rejection-log.js +5 -1
  55. package/src/core/rejection-log.js.map +1 -1
  56. package/src/index.d.ts +1 -1
  57. package/src/index.js +4 -1
  58. package/src/index.js.map +1 -1
  59. package/templates/ai-hook.sh +15 -1
@@ -0,0 +1,99 @@
1
+ /**
2
+ * CODEX HOOK TRUST — read it, report it, NEVER write it.
3
+ *
4
+ * ─── What Codex actually does ─────────────────────────────────────────────────────────────────────
5
+ * Codex trusts a hook entry TOFU. On first sight of a new or changed entry it PROMPTS
6
+ * (`Hooks need review / 1 hook is new or changed`) and, if the human accepts, records a hash in
7
+ * `~/.codex/config.toml`:
8
+ *
9
+ * [hooks.state."<abs path to .codex/hooks.json>:pre_tool_use:<group>:<index>"]
10
+ * trusted_hash = "sha256:…"
11
+ *
12
+ * ─── Why this only ever REPORTS ───────────────────────────────────────────────────────────────────
13
+ * The hash is NOT reproducible from outside Codex. Sixteen encodings were tried against a hooks.json we
14
+ * wrote ourselves and none of them produced the recorded value. So an installer that tried to write one
15
+ * would be guessing at a security decision on a human's behalf, and would either be ignored or — worse —
16
+ * appear to succeed. Forging it is not on the table even if the encoding were known: the prompt IS the
17
+ * mechanism, and the whole point of TOFU is that a person saw the command once.
18
+ *
19
+ * The consequence the installer must therefore SAY OUT LOUD: the prompt's third option is
20
+ * `Continue without trusting (hooks won't run)`. That is one keystroke to a fully unguarded session,
21
+ * with no later warning of any kind. Install-time verification cannot see that choice — it happens
22
+ * afterwards, in another process — which is exactly why guard-presence attestation exists as a separate
23
+ * mechanism (see ./codex-guard-presence.ts) rather than being folded into this check.
24
+ */
25
+ /** What one repo's Codex trust looks like right now. Data-only → a class, per CLAUDE.md. */
26
+ export declare class CodexTrustStatus {
27
+ /** `~/.codex/config.toml`, named even when it does not exist so the report can point at it. */
28
+ readonly configPath: string;
29
+ readonly configExists: boolean;
30
+ /** `[projects."<root>"] trust_level = "trusted"`. */
31
+ readonly projectTrusted: boolean;
32
+ /** The repo's `.codex/hooks.json`, absolute — the key every `hooks.state` entry is prefixed by. */
33
+ readonly hooksPath: string;
34
+ /** How many webpieces-managed PreToolUse entries that file registers. 0 ⇒ Codex is not armed. */
35
+ readonly registeredEntries: number;
36
+ /** How many of them `~/.codex/config.toml` records a `trusted_hash` for. */
37
+ readonly trustedEntries: number;
38
+ constructor(
39
+ /** `~/.codex/config.toml`, named even when it does not exist so the report can point at it. */
40
+ configPath: string, configExists: boolean,
41
+ /** `[projects."<root>"] trust_level = "trusted"`. */
42
+ projectTrusted: boolean,
43
+ /** The repo's `.codex/hooks.json`, absolute — the key every `hooks.state` entry is prefixed by. */
44
+ hooksPath: string,
45
+ /** How many webpieces-managed PreToolUse entries that file registers. 0 ⇒ Codex is not armed. */
46
+ registeredEntries: number,
47
+ /** How many of them `~/.codex/config.toml` records a `trusted_hash` for. */
48
+ trustedEntries: number);
49
+ /** True when Codex is armed here AND every entry it would run is trusted. */
50
+ fullyTrusted(): boolean;
51
+ /** True when there is nothing to say — this repo has not armed Codex at all. */
52
+ notArmed(): boolean;
53
+ /**
54
+ * The report, as lines. It states what IS, then the one action that changes it — and the action is
55
+ * always the human's, because nothing here can be repaired by a tool.
56
+ */
57
+ lines(): readonly string[];
58
+ }
59
+ /**
60
+ * Reads Codex's trust state. Never writes, never throws — an unreadable or absent config reports as
61
+ * "not trusted", which is the true and safe answer.
62
+ */
63
+ export declare class CodexTrustProbe {
64
+ /** `~/.codex/config.toml`. `homeDir` is injectable so a unit test never reads the real one. */
65
+ configPath(homeDir?: string): string;
66
+ read(projectRoot: string, homeDir?: string): CodexTrustStatus;
67
+ /**
68
+ * How many webpieces-managed PreToolUse commands `.codex/hooks.json` registers.
69
+ *
70
+ * It counts what WE own, not every hook in the file: a consumer's own unrelated hook is none of this
71
+ * check's business, and counting it would make the report say "1 of 3 trusted" about hooks webpieces
72
+ * neither wrote nor can advise on.
73
+ */
74
+ private registeredEntryCount;
75
+ private readOrEmpty;
76
+ /**
77
+ * `[projects."<root>"]` carrying `trust_level = "trusted"`.
78
+ *
79
+ * A LINE SCAN, not a TOML parser, and deliberately: this package must load on a tree too broken to
80
+ * build a DI container, so it takes no dependency it does not need, and the two shapes it reads are
81
+ * both written by Codex itself in one fixed form. It over-reports "not trusted" for anything it
82
+ * cannot recognise, which is the safe direction — the consequence is one advisory line too many,
83
+ * never a session reported as guarded when it is not.
84
+ */
85
+ private projectTrusted;
86
+ /**
87
+ * How many `hooks.state."<hooksPath>:pre_tool_use:<group>:<index>"` sections carry a `trusted_hash`.
88
+ *
89
+ * The count is compared against the number of entries the file registers rather than matched
90
+ * one-for-one to a specific group/index: the group and index are Codex's own numbering of a file it
91
+ * re-reads on every launch, and an installer that assumed a numbering would report a false alarm the
92
+ * first time Codex renumbered.
93
+ */
94
+ private trustedEntryCount;
95
+ /** The body lines of one TOML section, or [] when the file carries no such header. */
96
+ private sectionBody;
97
+ /** The `<name>` of a `[<name>]` header line, or null for anything else. */
98
+ private headerOf;
99
+ }
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CodexTrustProbe = exports.CodexTrustStatus = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const os_1 = require("os");
8
+ const to_error_1 = require("../core/to-error");
9
+ const hook_registration_1 = require("./hook-registration");
10
+ /**
11
+ * CODEX HOOK TRUST — read it, report it, NEVER write it.
12
+ *
13
+ * ─── What Codex actually does ─────────────────────────────────────────────────────────────────────
14
+ * Codex trusts a hook entry TOFU. On first sight of a new or changed entry it PROMPTS
15
+ * (`Hooks need review / 1 hook is new or changed`) and, if the human accepts, records a hash in
16
+ * `~/.codex/config.toml`:
17
+ *
18
+ * [hooks.state."<abs path to .codex/hooks.json>:pre_tool_use:<group>:<index>"]
19
+ * trusted_hash = "sha256:…"
20
+ *
21
+ * ─── Why this only ever REPORTS ───────────────────────────────────────────────────────────────────
22
+ * The hash is NOT reproducible from outside Codex. Sixteen encodings were tried against a hooks.json we
23
+ * wrote ourselves and none of them produced the recorded value. So an installer that tried to write one
24
+ * would be guessing at a security decision on a human's behalf, and would either be ignored or — worse —
25
+ * appear to succeed. Forging it is not on the table even if the encoding were known: the prompt IS the
26
+ * mechanism, and the whole point of TOFU is that a person saw the command once.
27
+ *
28
+ * The consequence the installer must therefore SAY OUT LOUD: the prompt's third option is
29
+ * `Continue without trusting (hooks won't run)`. That is one keystroke to a fully unguarded session,
30
+ * with no later warning of any kind. Install-time verification cannot see that choice — it happens
31
+ * afterwards, in another process — which is exactly why guard-presence attestation exists as a separate
32
+ * mechanism (see ./codex-guard-presence.ts) rather than being folded into this check.
33
+ */
34
+ /** What one repo's Codex trust looks like right now. Data-only → a class, per CLAUDE.md. */
35
+ class CodexTrustStatus {
36
+ configPath;
37
+ configExists;
38
+ projectTrusted;
39
+ hooksPath;
40
+ registeredEntries;
41
+ trustedEntries;
42
+ // eslint-disable-next-line @typescript-eslint/max-params
43
+ constructor(
44
+ /** `~/.codex/config.toml`, named even when it does not exist so the report can point at it. */
45
+ configPath, configExists,
46
+ /** `[projects."<root>"] trust_level = "trusted"`. */
47
+ projectTrusted,
48
+ /** The repo's `.codex/hooks.json`, absolute — the key every `hooks.state` entry is prefixed by. */
49
+ hooksPath,
50
+ /** How many webpieces-managed PreToolUse entries that file registers. 0 ⇒ Codex is not armed. */
51
+ registeredEntries,
52
+ /** How many of them `~/.codex/config.toml` records a `trusted_hash` for. */
53
+ trustedEntries) {
54
+ this.configPath = configPath;
55
+ this.configExists = configExists;
56
+ this.projectTrusted = projectTrusted;
57
+ this.hooksPath = hooksPath;
58
+ this.registeredEntries = registeredEntries;
59
+ this.trustedEntries = trustedEntries;
60
+ }
61
+ /** True when Codex is armed here AND every entry it would run is trusted. */
62
+ fullyTrusted() {
63
+ return this.registeredEntries > 0 && this.trustedEntries >= this.registeredEntries && this.projectTrusted;
64
+ }
65
+ /** True when there is nothing to say — this repo has not armed Codex at all. */
66
+ notArmed() {
67
+ return this.registeredEntries === 0;
68
+ }
69
+ /**
70
+ * The report, as lines. It states what IS, then the one action that changes it — and the action is
71
+ * always the human's, because nothing here can be repaired by a tool.
72
+ */
73
+ lines() {
74
+ if (this.notArmed())
75
+ return [];
76
+ if (this.fullyTrusted()) {
77
+ return [` ✅ Codex trusts all ${String(this.registeredEntries)} webpieces hook(s) in ${this.hooksPath}`];
78
+ }
79
+ const out = [];
80
+ out.push(` ⚠️ Codex has NOT yet trusted the webpieces hooks in ${this.hooksPath}`);
81
+ if (!this.configExists) {
82
+ out.push(` ${this.configPath} does not exist yet — Codex has never run for this user.`);
83
+ }
84
+ else if (!this.projectTrusted) {
85
+ out.push(' this project is not marked trusted in ~/.codex/config.toml.');
86
+ }
87
+ if (this.trustedEntries < this.registeredEntries) {
88
+ out.push(` ${String(this.trustedEntries)} of ${String(this.registeredEntries)} hook entries carry a trusted_hash.`);
89
+ }
90
+ out.push(' Fix: run `codex` in this repo and choose "Trust all" when it asks about hooks.');
91
+ out.push(' Until you do, its third option ("Continue without trusting") leaves the session UNGUARDED —');
92
+ out.push(' which is what the guard-presence check on the build exists to catch.');
93
+ return out;
94
+ }
95
+ }
96
+ exports.CodexTrustStatus = CodexTrustStatus;
97
+ /**
98
+ * Reads Codex's trust state. Never writes, never throws — an unreadable or absent config reports as
99
+ * "not trusted", which is the true and safe answer.
100
+ */
101
+ class CodexTrustProbe {
102
+ /** `~/.codex/config.toml`. `homeDir` is injectable so a unit test never reads the real one. */
103
+ configPath(homeDir = (0, os_1.homedir)()) {
104
+ return path.join(homeDir, '.codex', 'config.toml');
105
+ }
106
+ read(projectRoot, homeDir = (0, os_1.homedir)()) {
107
+ const configPath = this.configPath(homeDir);
108
+ const hooksPath = path.join(projectRoot, ...hook_registration_1.CODEX_REGISTRATION.settingsFiles[0].split('/'));
109
+ const registered = this.registeredEntryCount(hooksPath);
110
+ const raw = this.readOrEmpty(configPath);
111
+ return new CodexTrustStatus(configPath, raw !== null, this.projectTrusted(raw ?? '', projectRoot), hooksPath, registered, this.trustedEntryCount(raw ?? '', hooksPath));
112
+ }
113
+ /**
114
+ * How many webpieces-managed PreToolUse commands `.codex/hooks.json` registers.
115
+ *
116
+ * It counts what WE own, not every hook in the file: a consumer's own unrelated hook is none of this
117
+ * check's business, and counting it would make the report say "1 of 3 trusted" about hooks webpieces
118
+ * neither wrote nor can advise on.
119
+ */
120
+ registeredEntryCount(hooksPath) {
121
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
122
+ try {
123
+ if (!fs.existsSync(hooksPath))
124
+ return 0;
125
+ const entries = (0, hook_registration_1.readSettings)(hooksPath).hooks?.PreToolUse ?? [];
126
+ return entries.reduce((total, entry) => total + entry.hooks.filter((h) => (0, hook_registration_1.isManagedCommand)(h.command)).length, 0);
127
+ }
128
+ catch (err) {
129
+ const error = (0, to_error_1.toError)(err);
130
+ void error; // an unreadable/invalid hooks.json is "not armed"; the drift check is what reports that
131
+ return 0;
132
+ }
133
+ }
134
+ readOrEmpty(configPath) {
135
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
136
+ try {
137
+ return fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : null;
138
+ }
139
+ catch (err) {
140
+ const error = (0, to_error_1.toError)(err);
141
+ void error; // unreadable → treated as absent, which reports as "not trusted"
142
+ return null;
143
+ }
144
+ }
145
+ /**
146
+ * `[projects."<root>"]` carrying `trust_level = "trusted"`.
147
+ *
148
+ * A LINE SCAN, not a TOML parser, and deliberately: this package must load on a tree too broken to
149
+ * build a DI container, so it takes no dependency it does not need, and the two shapes it reads are
150
+ * both written by Codex itself in one fixed form. It over-reports "not trusted" for anything it
151
+ * cannot recognise, which is the safe direction — the consequence is one advisory line too many,
152
+ * never a session reported as guarded when it is not.
153
+ */
154
+ projectTrusted(raw, projectRoot) {
155
+ return this.sectionBody(raw, `projects."${projectRoot}"`)
156
+ .some((line) => /^\s*trust_level\s*=\s*"trusted"\s*$/.test(line));
157
+ }
158
+ /**
159
+ * How many `hooks.state."<hooksPath>:pre_tool_use:<group>:<index>"` sections carry a `trusted_hash`.
160
+ *
161
+ * The count is compared against the number of entries the file registers rather than matched
162
+ * one-for-one to a specific group/index: the group and index are Codex's own numbering of a file it
163
+ * re-reads on every launch, and an installer that assumed a numbering would report a false alarm the
164
+ * first time Codex renumbered.
165
+ */
166
+ trustedEntryCount(raw, hooksPath) {
167
+ const prefix = `hooks.state."${hooksPath}:pre_tool_use:`;
168
+ let trusted = 0;
169
+ let inEntry = false;
170
+ for (const line of raw.split('\n')) {
171
+ const header = this.headerOf(line);
172
+ if (header !== null)
173
+ inEntry = header.startsWith(prefix);
174
+ else if (inEntry && /^\s*trusted_hash\s*=\s*"/.test(line))
175
+ trusted += 1;
176
+ }
177
+ return trusted;
178
+ }
179
+ /** The body lines of one TOML section, or [] when the file carries no such header. */
180
+ sectionBody(raw, wanted) {
181
+ const body = [];
182
+ let inside = false;
183
+ for (const line of raw.split('\n')) {
184
+ const header = this.headerOf(line);
185
+ if (header !== null)
186
+ inside = header === wanted;
187
+ else if (inside)
188
+ body.push(line);
189
+ }
190
+ return body;
191
+ }
192
+ /** The `<name>` of a `[<name>]` header line, or null for anything else. */
193
+ headerOf(line) {
194
+ const match = /^\s*\[([^\]]+)\]\s*$/.exec(line);
195
+ return match === null ? null : match[1];
196
+ }
197
+ }
198
+ exports.CodexTrustProbe = CodexTrustProbe;
199
+ //# sourceMappingURL=codex-trust.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-trust.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/codex-trust.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\n\nimport { toError } from '../core/to-error';\nimport { CODEX_REGISTRATION, HookCommand, HookEntry, isManagedCommand, readSettings } from './hook-registration';\n\n/**\n * CODEX HOOK TRUST — read it, report it, NEVER write it.\n *\n * ─── What Codex actually does ─────────────────────────────────────────────────────────────────────\n * Codex trusts a hook entry TOFU. On first sight of a new or changed entry it PROMPTS\n * (`Hooks need review / 1 hook is new or changed`) and, if the human accepts, records a hash in\n * `~/.codex/config.toml`:\n *\n * [hooks.state.\"<abs path to .codex/hooks.json>:pre_tool_use:<group>:<index>\"]\n * trusted_hash = \"sha256:…\"\n *\n * ─── Why this only ever REPORTS ───────────────────────────────────────────────────────────────────\n * The hash is NOT reproducible from outside Codex. Sixteen encodings were tried against a hooks.json we\n * wrote ourselves and none of them produced the recorded value. So an installer that tried to write one\n * would be guessing at a security decision on a human's behalf, and would either be ignored or — worse —\n * appear to succeed. Forging it is not on the table even if the encoding were known: the prompt IS the\n * mechanism, and the whole point of TOFU is that a person saw the command once.\n *\n * The consequence the installer must therefore SAY OUT LOUD: the prompt's third option is\n * `Continue without trusting (hooks won't run)`. That is one keystroke to a fully unguarded session,\n * with no later warning of any kind. Install-time verification cannot see that choice — it happens\n * afterwards, in another process — which is exactly why guard-presence attestation exists as a separate\n * mechanism (see ./codex-guard-presence.ts) rather than being folded into this check.\n */\n\n/** What one repo's Codex trust looks like right now. Data-only → a class, per CLAUDE.md. */\nexport class CodexTrustStatus {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n /** `~/.codex/config.toml`, named even when it does not exist so the report can point at it. */\n readonly configPath: string,\n readonly configExists: boolean,\n /** `[projects.\"<root>\"] trust_level = \"trusted\"`. */\n readonly projectTrusted: boolean,\n /** The repo's `.codex/hooks.json`, absolute — the key every `hooks.state` entry is prefixed by. */\n readonly hooksPath: string,\n /** How many webpieces-managed PreToolUse entries that file registers. 0 ⇒ Codex is not armed. */\n readonly registeredEntries: number,\n /** How many of them `~/.codex/config.toml` records a `trusted_hash` for. */\n readonly trustedEntries: number,\n ) {}\n\n /** True when Codex is armed here AND every entry it would run is trusted. */\n fullyTrusted(): boolean {\n return this.registeredEntries > 0 && this.trustedEntries >= this.registeredEntries && this.projectTrusted;\n }\n\n /** True when there is nothing to say — this repo has not armed Codex at all. */\n notArmed(): boolean {\n return this.registeredEntries === 0;\n }\n\n /**\n * The report, as lines. It states what IS, then the one action that changes it — and the action is\n * always the human's, because nothing here can be repaired by a tool.\n */\n lines(): readonly string[] {\n if (this.notArmed()) return [];\n if (this.fullyTrusted()) {\n return [` ✅ Codex trusts all ${String(this.registeredEntries)} webpieces hook(s) in ${this.hooksPath}`];\n }\n const out: string[] = [];\n out.push(` ⚠️ Codex has NOT yet trusted the webpieces hooks in ${this.hooksPath}`);\n if (!this.configExists) {\n out.push(` ${this.configPath} does not exist yet — Codex has never run for this user.`);\n } else if (!this.projectTrusted) {\n out.push(' this project is not marked trusted in ~/.codex/config.toml.');\n }\n if (this.trustedEntries < this.registeredEntries) {\n out.push(` ${String(this.trustedEntries)} of ${String(this.registeredEntries)} hook entries carry a trusted_hash.`);\n }\n out.push(' Fix: run `codex` in this repo and choose \"Trust all\" when it asks about hooks.');\n out.push(' Until you do, its third option (\"Continue without trusting\") leaves the session UNGUARDED —');\n out.push(' which is what the guard-presence check on the build exists to catch.');\n return out;\n }\n}\n\n/**\n * Reads Codex's trust state. Never writes, never throws — an unreadable or absent config reports as\n * \"not trusted\", which is the true and safe answer.\n */\nexport class CodexTrustProbe {\n /** `~/.codex/config.toml`. `homeDir` is injectable so a unit test never reads the real one. */\n configPath(homeDir: string = homedir()): string {\n return path.join(homeDir, '.codex', 'config.toml');\n }\n\n read(projectRoot: string, homeDir: string = homedir()): CodexTrustStatus {\n const configPath = this.configPath(homeDir);\n const hooksPath = path.join(projectRoot, ...CODEX_REGISTRATION.settingsFiles[0].split('/'));\n const registered = this.registeredEntryCount(hooksPath);\n const raw = this.readOrEmpty(configPath);\n return new CodexTrustStatus(\n configPath, raw !== null, this.projectTrusted(raw ?? '', projectRoot),\n hooksPath, registered, this.trustedEntryCount(raw ?? '', hooksPath),\n );\n }\n\n /**\n * How many webpieces-managed PreToolUse commands `.codex/hooks.json` registers.\n *\n * It counts what WE own, not every hook in the file: a consumer's own unrelated hook is none of this\n * check's business, and counting it would make the report say \"1 of 3 trusted\" about hooks webpieces\n * neither wrote nor can advise on.\n */\n private registeredEntryCount(hooksPath: string): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n if (!fs.existsSync(hooksPath)) return 0;\n const entries: readonly HookEntry[] = readSettings(hooksPath).hooks?.PreToolUse ?? [];\n return entries.reduce(\n (total: number, entry: HookEntry): number =>\n total + entry.hooks.filter((h: HookCommand): boolean => isManagedCommand(h.command)).length,\n 0,\n );\n } catch (err: unknown) {\n const error = toError(err);\n void error; // an unreadable/invalid hooks.json is \"not armed\"; the drift check is what reports that\n return 0;\n }\n }\n\n private readOrEmpty(configPath: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : null;\n } catch (err: unknown) {\n const error = toError(err);\n void error; // unreadable → treated as absent, which reports as \"not trusted\"\n return null;\n }\n }\n\n /**\n * `[projects.\"<root>\"]` carrying `trust_level = \"trusted\"`.\n *\n * A LINE SCAN, not a TOML parser, and deliberately: this package must load on a tree too broken to\n * build a DI container, so it takes no dependency it does not need, and the two shapes it reads are\n * both written by Codex itself in one fixed form. It over-reports \"not trusted\" for anything it\n * cannot recognise, which is the safe direction — the consequence is one advisory line too many,\n * never a session reported as guarded when it is not.\n */\n private projectTrusted(raw: string, projectRoot: string): boolean {\n return this.sectionBody(raw, `projects.\"${projectRoot}\"`)\n .some((line: string): boolean => /^\\s*trust_level\\s*=\\s*\"trusted\"\\s*$/.test(line));\n }\n\n /**\n * How many `hooks.state.\"<hooksPath>:pre_tool_use:<group>:<index>\"` sections carry a `trusted_hash`.\n *\n * The count is compared against the number of entries the file registers rather than matched\n * one-for-one to a specific group/index: the group and index are Codex's own numbering of a file it\n * re-reads on every launch, and an installer that assumed a numbering would report a false alarm the\n * first time Codex renumbered.\n */\n private trustedEntryCount(raw: string, hooksPath: string): number {\n const prefix = `hooks.state.\"${hooksPath}:pre_tool_use:`;\n let trusted = 0;\n let inEntry = false;\n for (const line of raw.split('\\n')) {\n const header = this.headerOf(line);\n if (header !== null) inEntry = header.startsWith(prefix);\n else if (inEntry && /^\\s*trusted_hash\\s*=\\s*\"/.test(line)) trusted += 1;\n }\n return trusted;\n }\n\n /** The body lines of one TOML section, or [] when the file carries no such header. */\n private sectionBody(raw: string, wanted: string): readonly string[] {\n const body: string[] = [];\n let inside = false;\n for (const line of raw.split('\\n')) {\n const header = this.headerOf(line);\n if (header !== null) inside = header === wanted;\n else if (inside) body.push(line);\n }\n return body;\n }\n\n /** The `<name>` of a `[<name>]` header line, or null for anything else. */\n private headerOf(line: string): string | null {\n const match = /^\\s*\\[([^\\]]+)\\]\\s*$/.exec(line);\n return match === null ? null : match[1];\n }\n}\n"]}