javi-forge 1.38.2 → 1.38.4

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.
@@ -177,6 +177,97 @@ function lex(command, powershell = false) {
177
177
  if (!commands.at(-1).length) commands.pop();
178
178
  return { commands, separators };
179
179
  }
180
+ function hasUnquotedHereDocOperator(line) {
181
+ let quote = "", escaped = false;
182
+ for (let index = 0; index < line.length; index++) {
183
+ const char = line[index];
184
+ if (escaped) { escaped = false; continue; }
185
+ if (quote) {
186
+ if (char === "\\" && quote === '"') escaped = true;
187
+ else if (char === quote) quote = "";
188
+ continue;
189
+ }
190
+ if (char === "\\") { escaped = true; continue; }
191
+ if (char === "'" || char === '"') { quote = char; continue; }
192
+ if (char === "#" && (index === 0 || /\s/.test(line[index - 1]))) break;
193
+ if (char === "<" && line[index + 1] === "<" && line[index + 2] !== "<") return true;
194
+ }
195
+ return false;
196
+ }
197
+ const SHELL_HEADER_WHITESPACE = new Set([" ", "\t"]);
198
+ const STATIC_FILE_TARGET_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._/-");
199
+ const HEREDOC_DELIMITER_START_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_");
200
+ const HEREDOC_DELIMITER_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_");
201
+ function isShellHeaderWhitespace(char) {
202
+ return SHELL_HEADER_WHITESPACE.has(char);
203
+ }
204
+ function skipShellWhitespace(line, index) {
205
+ while (isShellHeaderWhitespace(line[index])) index++;
206
+ return index;
207
+ }
208
+ function parseStaticFileTarget(line, index) {
209
+ const start = index;
210
+ while (index < line.length && !isShellHeaderWhitespace(line[index])) {
211
+ if (!STATIC_FILE_TARGET_CHARS.has(line[index])) return null;
212
+ index++;
213
+ }
214
+ return index === start ? null : { target: line.slice(start, index), index };
215
+ }
216
+ function parseQuotedHereDocDelimiter(line, index) {
217
+ const quote = line[index];
218
+ if (quote !== "'" && quote !== '"') return null;
219
+ const start = ++index;
220
+ while (index < line.length && line[index] !== quote) {
221
+ const char = line[index];
222
+ if (!(index === start ? HEREDOC_DELIMITER_START_CHARS : HEREDOC_DELIMITER_CHARS).has(char)) return null;
223
+ index++;
224
+ }
225
+ return index === start || line[index] !== quote ? null : { delimiter: line.slice(start, index), index: index + 1 };
226
+ }
227
+ function parseInertCatHereDocHeader(line) {
228
+ let index = skipShellWhitespace(line, 0);
229
+ if (line.slice(index, index + 3) !== "cat" || !(isShellHeaderWhitespace(line[index + 3]) || line[index + 3] === ">")) return null;
230
+ index = skipShellWhitespace(line, index + 3);
231
+ let target, delimiter;
232
+ if (line[index] === ">") {
233
+ const parsedTarget = parseStaticFileTarget(line, skipShellWhitespace(line, index + 1));
234
+ if (!parsedTarget) return null;
235
+ target = parsedTarget.target;
236
+ index = skipShellWhitespace(line, parsedTarget.index);
237
+ if (line[index] !== "<" || line[index + 1] !== "<" || line[index + 2] === "-") return null;
238
+ const parsedDelimiter = parseQuotedHereDocDelimiter(line, skipShellWhitespace(line, index + 2));
239
+ if (!parsedDelimiter) return null;
240
+ delimiter = parsedDelimiter.delimiter;
241
+ index = parsedDelimiter.index;
242
+ } else if (line[index] === "<" && line[index + 1] === "<" && line[index + 2] !== "-") {
243
+ const parsedDelimiter = parseQuotedHereDocDelimiter(line, skipShellWhitespace(line, index + 2));
244
+ if (!parsedDelimiter) return null;
245
+ delimiter = parsedDelimiter.delimiter;
246
+ index = skipShellWhitespace(line, parsedDelimiter.index);
247
+ if (line[index] !== ">") return null;
248
+ const parsedTarget = parseStaticFileTarget(line, skipShellWhitespace(line, index + 1));
249
+ if (!parsedTarget) return null;
250
+ target = parsedTarget.target;
251
+ index = parsedTarget.index;
252
+ } else return null;
253
+ return skipShellWhitespace(line, index) === line.length ? { target, delimiter } : null;
254
+ }
255
+ function opaqueInertCatHereDoc(command, cwd, config, projectRoot) {
256
+ // A continuation changes the lexical boundary which identifies a heredoc
257
+ // header. This bounded parser cannot prove it is body-only, so fail closed.
258
+ if (command.includes("\\" + "\n") || command.includes("\\" + "\r\n")) return null;
259
+ const lines = command.split(/\r?\n/);
260
+ if (lines.at(-1) === "") lines.pop();
261
+ const parsed = parseInertCatHereDocHeader(lines[0] ?? "");
262
+ if (parsed && lines.length >= 2) {
263
+ const terminator = lines.findIndex((line, index) => index > 0 && line === parsed.delimiter);
264
+ if (terminator === lines.length - 1) {
265
+ const target = lexicalizePolicyPath(parsed.target, { base: cwd, projectRoot });
266
+ return evaluateFile("Write", target, config, projectRoot).allowed ? lines[0] : null;
267
+ }
268
+ }
269
+ return lines.some(hasUnquotedHereDocOperator) ? null : command;
270
+ }
180
271
  // The pre-redesign boolean helpers (parseEnvSplit/hasChmodRecursive/hasBase64Decode
181
272
  // and their isLongPrefix/splitEnvString internals) were replaced by the semantic
182
273
  // state machines below; only ENV_ESCAPES survives, shared with splitEnvSemantics.
@@ -703,6 +794,9 @@ function bashSubstitutions(command) {
703
794
  }
704
795
  function evaluateBash(command, cwd, config = AGENT_CONFIGS.claude, projectRoot = PROJECT_ROOT, depth = 0) {
705
796
  if (depth > 4) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
797
+ const headerOnlyCommand = opaqueInertCatHereDoc(command, cwd, config, projectRoot);
798
+ if (headerOnlyCommand === null) return { allowed: false, ruleId: "shell.obfuscated-interpreter" };
799
+ command = headerOnlyCommand;
706
800
  try { for (const body of bashSubstitutions(command)) { const nested = evaluateBash(body, cwd, config, projectRoot, depth + 1); if (!nested.allowed) return nested; } } catch { return { allowed: false, ruleId: "shell.obfuscated-interpreter" }; }
707
801
  if (/^\s*:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:\s*$/.test(command)) return { allowed: false, ruleId: "shell.destructive-root" };
708
802
  let parsed;
@@ -4,14 +4,15 @@
4
4
  "name": "javi-forge-skillguard-pre-tool-use.mjs",
5
5
  "version": 1,
6
6
  "policyVersion": 2,
7
- "sha256": "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1",
7
+ "sha256": "6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af",
8
8
  "historical": [
9
9
  "78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc",
10
10
  "5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425",
11
11
  "0c9aa8fa26b389f4892782f83104f0792c2b71ebca39c18c02706e2185e22b40",
12
12
  "3581862f0567cce75a58b693c9ade80d39ee7d58add11537a34a8461c47c1ed4",
13
13
  "54a270f28b068450b79547a88ec6f2d4854514392fd5f38ed1d6174ea093d7aa",
14
- "9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e"
14
+ "9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e",
15
+ "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1"
15
16
  ]
16
17
  },
17
18
  "settingsEntries": {
@@ -19,7 +19,7 @@ export declare const CI_HELP_TEXT = "\n Usage\n $ javi-forge ci [subcommand]
19
19
  * Per-command help for `hooks`, shown by `javi-forge hooks --help` (or when
20
20
  * `hooks` is given an unknown subcommand). Whitespace is significant.
21
21
  */
22
- export declare const HOOKS_HELP_TEXT = "\n Usage\n $ javi-forge hooks run <pre-commit|pre-push>\n $ javi-forge hooks <install|doctor|repair> claude [--force]\n\n Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed\n cheap\u2192expensive order, fail-fast. With no hooks: config the default is the\n quick native CI gate (setup + lint + compile + gates \u2014 no tests, no coverage).\n\n Subcommands\n run pre-commit Run the composed pre-commit sections\n run pre-push Run the composed pre-push sections\n install claude Install the managed Claude PreToolUse guard (.claude/)\n doctor claude Report Claude PreToolUse guard health (informational)\n repair claude Repair the managed guard; --force overwrites edited assets\n\n Notes\n A blocking section failure exits non-zero and blocks the commit/push.\n A broken .javi-forge/ci.yaml exits 1 (fail-closed \u2014 never skips a gate).\n To skip: git commit --no-verify (pre-push: git push --no-verify)\n doctor claude is informational (always exits 0); install/repair exit 0 on\n success, non-zero on refusal/failure. Use repair claude --force to overwrite\n a locally edited managed asset.\n Linux: install/repair claude need the acl package (getfacl) to prove the\n parent chain \u2014 apt install acl / apk add acl / dnf install acl. Without it\n they refuse fail-closed; an already-installed guard keeps firing, and\n doctor claude reports the acl capability as its own row.\n Claude Code spawns the guard with node from ITS path, so node must resolve\n there, not only inside javi-forge.\n\n Examples\n $ javi-forge hooks run pre-commit\n $ javi-forge hooks run pre-push\n $ javi-forge hooks install claude\n $ javi-forge hooks doctor claude\n $ javi-forge hooks repair claude --force\n";
22
+ export declare const HOOKS_HELP_TEXT = "\n Usage\n $ javi-forge hooks run <pre-commit|pre-push>\n $ javi-forge hooks <install|doctor|repair> <claude|codex> [--force]\n\n Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed\n cheap\u2192expensive order, fail-fast. With no hooks: config the default is the\n quick native CI gate (setup + lint + compile + gates \u2014 no tests, no coverage).\n\n Subcommands\n run pre-commit Run the composed pre-commit sections\n run pre-push Run the composed pre-push sections\n install claude Install the managed Claude PreToolUse guard (.claude/)\n doctor claude Report Claude PreToolUse guard health (informational)\n repair claude Repair the managed guard; --force overwrites edited assets\n install codex Install the managed Codex PreToolUse guard (~/.codex/)\n doctor codex Report Codex hook execution readiness and trust boundary\n repair codex Repair the managed Codex guard; --force overwrites edits\n\n Notes\n A blocking section failure exits non-zero and blocks the commit/push.\n A broken .javi-forge/ci.yaml exits 1 (fail-closed \u2014 never skips a gate).\n To skip: git commit --no-verify (pre-push: git push --no-verify)\n doctor claude is informational (always exits 0); install/repair exit 0 on\n success, non-zero on refusal/failure. Use repair claude --force to overwrite\n a locally edited managed asset.\n doctor codex exits 0 when runnable, 1 when blocked, and 2 when inconclusive;\n it does not prove provider trust or runtime execution. Use repair codex\n --force only to overwrite an edited managed asset.\n Linux: install/repair claude need the acl package (getfacl) to prove the\n parent chain \u2014 apt install acl / apk add acl / dnf install acl. Without it\n they refuse fail-closed; an already-installed guard keeps firing, and\n doctor claude reports the acl capability as its own row.\n Claude Code spawns the guard with node from ITS path, so node must resolve\n there, not only inside javi-forge.\n\n Examples\n $ javi-forge hooks run pre-commit\n $ javi-forge hooks run pre-push\n $ javi-forge hooks install claude\n $ javi-forge hooks doctor claude\n $ javi-forge hooks repair claude --force\n $ javi-forge hooks install codex\n $ javi-forge hooks doctor codex\n $ javi-forge hooks repair codex --force\n";
23
23
  export declare const FLAGS_SCHEMA: {
24
24
  readonly help: {
25
25
  readonly type: "boolean";
package/dist/cli/help.js CHANGED
@@ -166,7 +166,7 @@ export const CI_HELP_TEXT = `
166
166
  export const HOOKS_HELP_TEXT = `
167
167
  Usage
168
168
  $ javi-forge hooks run <pre-commit|pre-push>
169
- $ javi-forge hooks <install|doctor|repair> claude [--force]
169
+ $ javi-forge hooks <install|doctor|repair> <claude|codex> [--force]
170
170
 
171
171
  Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed
172
172
  cheap→expensive order, fail-fast. With no hooks: config the default is the
@@ -178,6 +178,9 @@ export const HOOKS_HELP_TEXT = `
178
178
  install claude Install the managed Claude PreToolUse guard (.claude/)
179
179
  doctor claude Report Claude PreToolUse guard health (informational)
180
180
  repair claude Repair the managed guard; --force overwrites edited assets
181
+ install codex Install the managed Codex PreToolUse guard (~/.codex/)
182
+ doctor codex Report Codex hook execution readiness and trust boundary
183
+ repair codex Repair the managed Codex guard; --force overwrites edits
181
184
 
182
185
  Notes
183
186
  A blocking section failure exits non-zero and blocks the commit/push.
@@ -186,6 +189,9 @@ export const HOOKS_HELP_TEXT = `
186
189
  doctor claude is informational (always exits 0); install/repair exit 0 on
187
190
  success, non-zero on refusal/failure. Use repair claude --force to overwrite
188
191
  a locally edited managed asset.
192
+ doctor codex exits 0 when runnable, 1 when blocked, and 2 when inconclusive;
193
+ it does not prove provider trust or runtime execution. Use repair codex
194
+ --force only to overwrite an edited managed asset.
189
195
  Linux: install/repair claude need the acl package (getfacl) to prove the
190
196
  parent chain — apt install acl / apk add acl / dnf install acl. Without it
191
197
  they refuse fail-closed; an already-installed guard keeps firing, and
@@ -199,6 +205,9 @@ export const HOOKS_HELP_TEXT = `
199
205
  $ javi-forge hooks install claude
200
206
  $ javi-forge hooks doctor claude
201
207
  $ javi-forge hooks repair claude --force
208
+ $ javi-forge hooks install codex
209
+ $ javi-forge hooks doctor codex
210
+ $ javi-forge hooks repair codex --force
202
211
  `;
203
212
  export const FLAGS_SCHEMA = {
204
213
  // `--help` is handled manually (autoHelp is disabled at the entrypoint so
@@ -254,9 +254,10 @@ function buildCodexHooksContainer(assetPath) {
254
254
  };
255
255
  }
256
256
  /**
257
- * Merge our managed group into an existing container: drop any prior managed
258
- * groups (ours, by command regex) and append a fresh one, preserving every
259
- * foreign group. A fresh install (no container) yields the clean container.
257
+ * Merge our managed group into an existing container: remove only prior managed
258
+ * handlers (ours, by command regex), preserve foreign handlers in their groups,
259
+ * and append one fresh canonical managed group. A fresh install (no container)
260
+ * yields the clean container.
260
261
  */
261
262
  function mergeCodexHooks(existing, assetPath) {
262
263
  if (!isPlainObject(existing))
@@ -266,14 +267,24 @@ function mergeCodexHooks(existing, assetPath) {
266
267
  container.hooks = {};
267
268
  const hooks = container.hooks;
268
269
  const groups = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
269
- const kept = groups.filter((group) => {
270
- const list = isPlainObject(group) && Array.isArray(group.hooks) ? group.hooks : [];
271
- const isOurs = list.some((h) => isPlainObject(h) &&
270
+ const kept = [];
271
+ for (const group of groups) {
272
+ if (!isPlainObject(group) || !Array.isArray(group.hooks)) {
273
+ kept.push(group);
274
+ continue;
275
+ }
276
+ const foreign = group.hooks.filter((h) => !(isPlainObject(h) &&
272
277
  h.type === "command" &&
273
278
  typeof h.command === "string" &&
274
- CODEX_CMD_RE.test(h.command));
275
- return !isOurs;
276
- });
279
+ CODEX_CMD_RE.test(h.command)));
280
+ if (foreign.length === 0)
281
+ continue;
282
+ if (foreign.length === group.hooks.length) {
283
+ kept.push(group);
284
+ continue;
285
+ }
286
+ kept.push({ ...group, hooks: foreign });
287
+ }
277
288
  const fresh = buildCodexHooksContainer(assetPath).hooks;
278
289
  hooks.PreToolUse = [...kept, ...fresh.PreToolUse];
279
290
  return container;
@@ -372,7 +383,9 @@ export async function doctorCodexPreToolUse(homeDir, options = {}) {
372
383
  ? "inconclusive"
373
384
  : "runnable";
374
385
  const remediation = [];
375
- if (hooksJson.state === "absent" || asset.state !== "managed-current") {
386
+ if (hooksJson.state === "absent" ||
387
+ hooksJson.state === "released-outdated" ||
388
+ asset.state !== "managed-current") {
376
389
  remediation.push("install the codex guard with: javi-forge hooks install codex");
377
390
  }
378
391
  if (!trusted)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.38.2",
3
+ "version": "1.38.4",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {