javi-forge 1.39.5 → 1.41.0

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.
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  export const MANAGED_MARKER = "// javi-forge-managed: claude-pretooluse v1";
9
9
  export const INPUT_LIMIT_BYTES = 1_048_576;
10
10
  export const SUPPORTED_TOOLS = Object.freeze(["Bash", "PowerShell", "Read", "Write", "Edit"]);
11
+ export function mapGrokToolName(toolName, platform = process.platform) { return toolName === "run_terminal_command" ? (platform === "win32" ? "PowerShell" : "Bash") : toolName === "read_file" ? "Read" : toolName === "search_replace" ? "Edit" : toolName; }
11
12
  export const POLICY_REGISTRY = Object.freeze({ schemaVersion: 1, policyVersion: 2, diagnosticsMaxBytes: 240 });
12
13
  export function resolvePlatformSupport(platform = process.platform) { return platform === "linux" || platform === "win32" ? { supported: true } : { supported: false, reason: "unsupported-platform" }; }
13
14
  const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -15,6 +16,7 @@ const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
15
16
  // never to an event field or a command token. This is intentionally an exact,
16
17
  // two-file allowlist rather than a whole-home or whole-.codex policy.
17
18
  const HOST_HOME = os.homedir();
19
+ const GROK_HOME_ROOT = process.env.GROK_HOME ? path.resolve(process.env.GROK_HOME) : path.join(HOST_HOME, ".grok");
18
20
  // Per-agent adapter config (S0 core-extraction): every agent-specific input the guard needs
19
21
  // (the isManaged protected-path set, the project-dir source, the managed marker) is resolved by
20
22
  // the --agent selector instead of a baked-in literal. The pure evaluate*/utility engine stays
@@ -22,8 +24,11 @@ const HOST_HOME = os.homedir();
22
24
  // byte-identical; codex is defined so the map is agent-generic but wired end-to-end only in a later slice.
23
25
  const CLAUDE_MANAGED_SET = Object.freeze({ exact: Object.freeze([".claude/settings.json", ".claude/settings.local.json", ".claude/CLAUDE.md", "CLAUDE.md", ".javi-forge/ci.yaml"]), prefixes: Object.freeze([".claude/hooks/", ".claude/agents/", ".claude/skills/"]), caseFoldExact: Object.freeze(["claude.md", ".claude/claude.md"]) });
24
26
  const CODEX_MANAGED_SET = Object.freeze({ exact: Object.freeze([".codex/hooks.json", ".claude/settings.json", ".claude/settings.local.json", ".claude/CLAUDE.md", "CLAUDE.md", ".javi-forge/ci.yaml"]), prefixes: Object.freeze([".claude/hooks/", ".claude/agents/", ".claude/skills/"]), caseFoldExact: Object.freeze(["claude.md", ".claude/claude.md"]) });
27
+ const OPENCODE_MANAGED_SET = Object.freeze({ exact: Object.freeze(["opencode.json", "opencode.jsonc", ".opencode/opencode.json", ".opencode/opencode.jsonc", ".opencode/AGENTS.md", "AGENTS.md", ".javi-forge/ci.yaml"]), prefixes: Object.freeze([".opencode/plugins/", ".opencode/agents/", ".opencode/skills/", ".agents/skills/", ".claude/hooks/", ".claude/agents/", ".claude/skills/"]), caseFoldExact: Object.freeze(["agents.md", ".opencode/agents.md", "claude.md", ".claude/claude.md"]) });
28
+ const GROK_MANAGED_SET = Object.freeze({ exact: Object.freeze([".grok/config.toml", "AGENTS.md", ".claude/settings.json", ".claude/settings.local.json", ".claude/CLAUDE.md", "CLAUDE.md", ".javi-forge/ci.yaml"]), prefixes: Object.freeze([".grok/hooks/", ".claude/hooks/", ".claude/agents/", ".claude/skills/"]), caseFoldExact: Object.freeze(["agents.md", "claude.md", ".claude/claude.md"]) });
25
29
  const CODEX_GLOBAL_MANAGED_PATHS = Object.freeze([".codex/hooks.json", ".codex/config.toml"]);
26
- export const AGENT_CONFIGS = Object.freeze({ claude: Object.freeze({ id: "claude", managedSet: CLAUDE_MANAGED_SET, globalManagedPaths: Object.freeze([]), projectDir: Object.freeze({ envVar: "CLAUDE_PROJECT_DIR", fallback: "asset-root" }), marker: MANAGED_MARKER }), codex: Object.freeze({ id: "codex", managedSet: CODEX_MANAGED_SET, globalManagedPaths: CODEX_GLOBAL_MANAGED_PATHS, projectDir: Object.freeze({ envVar: null, fallback: "cwd" }), marker: "// javi-forge-managed: codex-pretooluse v1" }) });
30
+ const GROK_GLOBAL_MANAGED_PATHS = Object.freeze([path.join(GROK_HOME_ROOT, "hooks", "javi-forge-skillguard-pre-tool-use.json"), path.join(GROK_HOME_ROOT, "hooks", "javi-forge-skillguard-pre-tool-use.mjs")]);
31
+ export const AGENT_CONFIGS = Object.freeze({ claude: Object.freeze({ id: "claude", managedSet: CLAUDE_MANAGED_SET, globalManagedPaths: Object.freeze([]), projectDir: Object.freeze({ envVar: "CLAUDE_PROJECT_DIR", fallback: "asset-root" }), marker: MANAGED_MARKER }), codex: Object.freeze({ id: "codex", managedSet: CODEX_MANAGED_SET, globalManagedPaths: CODEX_GLOBAL_MANAGED_PATHS, projectDir: Object.freeze({ envVar: null, fallback: "cwd" }), marker: "// javi-forge-managed: codex-pretooluse v1" }), opencode: Object.freeze({ id: "opencode", managedSet: OPENCODE_MANAGED_SET, globalManagedPaths: Object.freeze([]), projectDir: Object.freeze({ envVar: null, fallback: "cwd" }), marker: "// javi-forge-managed: opencode-skillguard v1" }), grok: Object.freeze({ id: "grok", managedSet: GROK_MANAGED_SET, globalManagedPaths: GROK_GLOBAL_MANAGED_PATHS, projectDir: Object.freeze({ envVar: null, fallback: "cwd" }), marker: "javi-forge-managed: grok-pretooluse v1" }) });
27
32
  // Fail-closed agent selector: a missing/unknown --agent means we cannot know what to protect, so refuse.
28
33
  function resolveAgentConfig(argv) { const arg = argv.find((value) => typeof value === "string" && value.startsWith("--agent=")); const id = arg === undefined ? undefined : arg.slice("--agent=".length); const config = id === undefined ? undefined : AGENT_CONFIGS[id]; if (!config) fail("invalid-config"); return config; }
29
34
  // Project root per agent: the env var when set (Claude = CLAUDE_PROJECT_DIR); otherwise the per-agent
@@ -86,7 +91,7 @@ export function lexicalizePolicyPath(input, options = {}) {
86
91
  let expanded = input;
87
92
  if (options.base) {
88
93
  expanded = expanded.replace(/^\$\{CLAUDE_PROJECT_DIR\}|^\$CLAUDE_PROJECT_DIR/, options.projectRoot ?? PROJECT_ROOT);
89
- expanded = expanded.replace(/^~(?=[\\/]|$)|^\$HOME(?=[\\/]|$)/, os.homedir());
94
+ expanded = expanded.replace(/^~(?=[\\/]|$)|^(?:\$HOME|\$\{HOME\})(?=[\\/]|$)/, os.homedir());
90
95
  if (!POSIX_ABSOLUTE.test(expanded) && !WINDOWS_DRIVE.test(expanded) && !WINDOWS_UNC.test(expanded)) {
91
96
  expanded = path.resolve(options.base, expanded);
92
97
  }
@@ -141,7 +146,7 @@ export function isSensitivePolicyKey(key, platform = process.platform) {
141
146
  return platform === "win32" ? basename.toLowerCase() === "serviceaccountkey.json" : basename === "serviceAccountKey.json";
142
147
  }
143
148
  function isManaged(key, managedSet = CLAUDE_MANAGED_SET, projectRoot = PROJECT_ROOT, globalManagedPaths = []) {
144
- if (globalManagedPaths.some((relative) => key === canonicalizePolicyPath(path.join(HOST_HOME, relative)))) return true;
149
+ if (globalManagedPaths.some((targetPath) => key === canonicalizePolicyPath(path.isAbsolute(targetPath) ? targetPath : path.join(HOST_HOME, targetPath)))) return true;
145
150
  const project = canonicalizePolicyPath(projectRoot);
146
151
  if (!key.startsWith(`${project}/`) && key !== project) return false;
147
152
  const relative = key.slice(project.length + 1);
@@ -951,30 +956,36 @@ export function parseApplyPatchPaths(command) {
951
956
  return paths;
952
957
  }
953
958
  export function evaluateEvent(input, config = AGENT_CONFIGS.claude) {
954
- if (!isObject(input) || input.hook_event_name !== "PreToolUse" || !isObject(input.tool_input)) fail("invalid-event");
955
- const applyPatch = input.tool_name === "apply_patch";
956
- if (!applyPatch && !SUPPORTED_TOOLS.includes(input.tool_name)) fail("invalid-event");
959
+ if (!isObject(input) || input.hook_event_name !== "PreToolUse") fail("invalid-event");
960
+ const rawToolName = typeof input.tool_name === "string" ? input.tool_name : input.toolName;
961
+ const toolName = config.id === "grok" && typeof rawToolName === "string" ? mapGrokToolName(rawToolName) : rawToolName;
962
+ const toolInput = isObject(input.tool_input) ? input.tool_input : input.toolInput;
963
+ if (typeof toolName !== "string" || !isObject(toolInput)) fail("invalid-event");
964
+ if (input.toolInputTruncated === true || input.tool_input_truncated === true) return { allowed: false, ruleId: "tool-input-truncated" };
965
+ const applyPatch = toolName === "apply_patch";
966
+ if (!applyPatch && !SUPPORTED_TOOLS.includes(toolName)) fail("invalid-event");
957
967
  const cwd = typeof input.cwd === "string" && isAbsolutePolicyPath(input.cwd) ? input.cwd : PROJECT_ROOT;
958
968
  const projectRoot = resolveProjectRoot(config, cwd);
959
969
  if (applyPatch) {
960
- if (typeof input.tool_input.command !== "string") fail("invalid-event");
970
+ if (typeof toolInput.command !== "string") fail("invalid-event");
961
971
  // apply_patch is a WRITE tool (not "Read"), so the managed-config rule fires. Each header
962
972
  // path is made absolute LEXICALLY (no realpath) vs the envelope cwd and handed to evaluateFile
963
973
  // exactly like Write/Edit's raw file_path, so policyPathKeys owns the dual lexical+realpath
964
974
  // canonicalization. Realpath-ing here first would collapse a symlinked managed dir onto its
965
975
  // target and drop the protective lexical key -> managed-config write bypass.
966
- for (const target of parseApplyPatchPaths(input.tool_input.command)) {
976
+ for (const target of parseApplyPatchPaths(toolInput.command)) {
967
977
  const decision = evaluateFile("apply_patch", lexicalizePolicyPath(target, { base: cwd, projectRoot }), config, projectRoot);
968
978
  if (!decision.allowed) return decision;
969
979
  }
970
980
  return { allowed: true };
971
981
  }
972
- if (input.tool_name === "Bash" || input.tool_name === "PowerShell") {
973
- if (typeof input.tool_input.command !== "string") fail("invalid-event");
974
- return input.tool_name === "Bash" ? evaluateBash(input.tool_input.command, cwd, config, projectRoot) : evaluatePowerShell(input.tool_input.command, cwd, config, projectRoot);
982
+ if (toolName === "Bash" || toolName === "PowerShell") {
983
+ if (typeof toolInput.command !== "string") fail("invalid-event");
984
+ return toolName === "Bash" ? evaluateBash(toolInput.command, cwd, config, projectRoot) : evaluatePowerShell(toolInput.command, cwd, config, projectRoot);
975
985
  }
976
- if (typeof input.tool_input.file_path !== "string" || !isAbsolutePolicyPath(input.tool_input.file_path)) fail("invalid-event");
977
- return evaluateFile(input.tool_name, input.tool_input.file_path, config, projectRoot);
986
+ const filePath = typeof toolInput.file_path === "string" ? toolInput.file_path : typeof toolInput.filePath === "string" ? toolInput.filePath : config.id === "grok" && typeof toolInput.path === "string" ? lexicalizePolicyPath(toolInput.path, { base: cwd, projectRoot }) : undefined;
987
+ if (typeof filePath !== "string" || !isAbsolutePolicyPath(filePath)) fail("invalid-event");
988
+ return evaluateFile(toolName, filePath, config, projectRoot);
978
989
  }
979
990
  export function parseAndEvaluateInput(input) {
980
991
  if (!Buffer.isBuffer(input) || input.length === 0) fail("invalid-json");
@@ -1071,7 +1082,9 @@ export async function main() {
1071
1082
  }
1072
1083
  if (fault === "evaluator-throw") throw new Error("internal-error");
1073
1084
  const decision = evaluateEvent(parsed, config);
1074
- if (!decision.allowed) denyAndExit(denialDiagnostic(parsed.tool_name, decision));
1085
+ const rawToolName = typeof parsed.tool_name === "string" ? parsed.tool_name : parsed.toolName;
1086
+ const toolName = config.id === "grok" && typeof rawToolName === "string" ? mapGrokToolName(rawToolName) : rawToolName;
1087
+ if (!decision.allowed) denyAndExit(denialDiagnostic(toolName, decision));
1075
1088
  process.exitCode = 0;
1076
1089
  } catch (error) {
1077
1090
  denyAndExit(diagnostic(error));
@@ -4,7 +4,7 @@
4
4
  "name": "javi-forge-skillguard-pre-tool-use.mjs",
5
5
  "version": 1,
6
6
  "policyVersion": 2,
7
- "sha256": "f55ece1aea76251ec005f2b731fa44a58b6245fef8150b58486ae062167a2377",
7
+ "sha256": "044f6816d18a2894532fde52a145f10347f617cbb361fc10075d04f885327d22",
8
8
  "historical": [
9
9
  "78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc",
10
10
  "5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425",
@@ -14,7 +14,11 @@
14
14
  "9a565cec31d9e091e3fb9420b86685f824733bc1ebe479f086b2b955aba6ef3e",
15
15
  "59fc4224975ad64cfc85bab50ec60d9bd4948070e9d41f42ab43e6d8231c19a1",
16
16
  "6edfbb31ce0551b27e38ae1ffd1daf9cc4bea54f2c86687c5124132af5b8c0af",
17
- "507a57a15f1b967103bb1eefe701a74813d5a9603fa7bcc15579a215b03621e3"
17
+ "507a57a15f1b967103bb1eefe701a74813d5a9603fa7bcc15579a215b03621e3",
18
+ "f55ece1aea76251ec005f2b731fa44a58b6245fef8150b58486ae062167a2377",
19
+ "6fe1ef55363e6e3f38253ca94b17ec50c942352c00d8431cd2ff2f6858c094b6",
20
+ "b79ee8dc3df525810b07215927590366ed2669255666fdc817e9637cf0e10cf2",
21
+ "a352b5b6c29fcdeecf2a014ab0e271cab9373119460134d65134f16a19222e47"
18
22
  ]
19
23
  },
20
24
  "settingsEntries": {
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "hook": {
4
+ "name": "javi-forge-skillguard-pre-tool-use.json",
5
+ "version": 1,
6
+ "matcher": "run_terminal_command|read_file|search_replace",
7
+ "historicalVersions": []
8
+ }
9
+ }
@@ -0,0 +1,127 @@
1
+ // javi-forge-managed: opencode-skillguard v1
2
+ async function importPolicyRuntime() {
3
+ try {
4
+ return await import("./javi-forge-skillguard-pre-tool-use.mjs");
5
+ } catch (error) {
6
+ if (
7
+ !isObject(error) ||
8
+ !("code" in error) ||
9
+ error.code !== "ERR_MODULE_NOT_FOUND"
10
+ )
11
+ throw error;
12
+ return import("../claude-hooks/javi-forge-skillguard-pre-tool-use.mjs");
13
+ }
14
+ }
15
+
16
+ const { AGENT_CONFIGS, evaluateEvent } = await importPolicyRuntime();
17
+
18
+ const TOOL_NAME_MAP = Object.freeze({
19
+ apply_patch: "apply_patch",
20
+ bash: "Bash",
21
+ edit: "Edit",
22
+ powershell: "PowerShell",
23
+ pwsh: "PowerShell",
24
+ read: "Read",
25
+ shell: "Bash",
26
+ write: "Write",
27
+ });
28
+
29
+ function isObject(value) {
30
+ return typeof value === "object" && value !== null;
31
+ }
32
+
33
+ function firstString(...values) {
34
+ for (const value of values) if (typeof value === "string") return value;
35
+ return undefined;
36
+ }
37
+
38
+ function mapToolName(tool) {
39
+ return typeof tool === "string" ? TOOL_NAME_MAP[tool] : undefined;
40
+ }
41
+
42
+ function normalizeOpenCodeToolEvent(input, output, fallbackCwd = process.cwd()) {
43
+ const tool = mapToolName(input?.tool);
44
+ if (tool === undefined) return null;
45
+ const args = isObject(output?.args)
46
+ ? output.args
47
+ : isObject(input?.input)
48
+ ? input.input
49
+ : isObject(input?.args)
50
+ ? input.args
51
+ : {};
52
+ const cwd = firstString(args.cwd, input.cwd, input.directory, fallbackCwd);
53
+ const toolInput =
54
+ tool === "Bash" || tool === "PowerShell" || tool === "apply_patch"
55
+ ? { command: firstString(args.command, args.cmd) }
56
+ : { file_path: firstString(args.file_path, args.filePath, args.path, args.file) };
57
+ return {
58
+ hook_event_name: "PreToolUse",
59
+ tool_name: tool,
60
+ tool_input: toolInput,
61
+ cwd,
62
+ };
63
+ }
64
+
65
+ export function evaluateOpenCodeToolEvent(
66
+ input,
67
+ output,
68
+ fallbackCwd = process.cwd(),
69
+ ) {
70
+ const event = normalizeOpenCodeToolEvent(input, output, fallbackCwd);
71
+ if (event === null) return { allowed: true };
72
+ return evaluateEvent(event, AGENT_CONFIGS.opencode);
73
+ }
74
+
75
+ function denyMessage(toolName, decision) {
76
+ const tool = typeof toolName === "string" ? toolName : "tool";
77
+ return `javi-forge OpenCode denied ${tool} [${decision.ruleId ?? "unknown"}]: global guard policy denied the invocation`;
78
+ }
79
+
80
+ function failClosedMessage(error) {
81
+ const id =
82
+ error instanceof Error && /^[a-z-]+$/.test(error.message)
83
+ ? error.message
84
+ : "internal-error";
85
+ return `javi-forge OpenCode failed closed [${id}]: policy evaluation could not complete`;
86
+ }
87
+
88
+ export async function guardOpenCodeTool(input, output, fallbackCwd) {
89
+ let decision;
90
+ try {
91
+ decision = evaluateOpenCodeToolEvent(input, output, fallbackCwd);
92
+ } catch (error) {
93
+ throw new Error(failClosedMessage(error));
94
+ }
95
+ if (!decision.allowed) throw new Error(denyMessage(input?.tool, decision));
96
+ }
97
+
98
+ function resolveFallbackCwd(ctx) {
99
+ return firstString(
100
+ ctx?.location?.project?.canonical,
101
+ ctx?.location?.project?.directory,
102
+ ctx?.worktree,
103
+ ctx?.directory,
104
+ process.cwd(),
105
+ );
106
+ }
107
+
108
+ export async function JaviForgeSkillGuard(ctx = {}) {
109
+ const fallbackCwd = resolveFallbackCwd(ctx);
110
+ return {
111
+ "tool.execute.before": async (input, output) =>
112
+ guardOpenCodeTool(input, output, fallbackCwd),
113
+ };
114
+ }
115
+
116
+ export default {
117
+ id: "javi-forge.skillguard",
118
+ async setup(ctx) {
119
+ const fallbackCwd = resolveFallbackCwd(ctx);
120
+ await ctx.tool.hook("execute.before", (event) =>
121
+ guardOpenCodeTool(event, undefined, fallbackCwd),
122
+ );
123
+ },
124
+ async server(ctx = {}) {
125
+ return JaviForgeSkillGuard(ctx);
126
+ },
127
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "plugin": {
4
+ "name": "javi-forge-skillguard-plugin.mjs",
5
+ "version": 1,
6
+ "sha256": "901feed0cb3b50a7b9b0e3e99290d49b8bd25fc85a305513f947d34726b428d5",
7
+ "historical": []
8
+ }
9
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Subcommands:
8
8
  * - `hooks run <pre-commit|pre-push>` → runHook, exits with its code.
9
- * - `hooks <install|doctor|repair> <claude|codex> [--force]` → the matching
9
+ * - `hooks <install|doctor|repair> <claude|codex|opencode> [--force]` → the matching
10
10
  * agent command, exits with its code (unknown/missing agent → usage + exit 1).
11
11
  * Any other subcommand or a missing name → usage + exit 1.
12
12
  *
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Subcommands:
8
8
  * - `hooks run <pre-commit|pre-push>` → runHook, exits with its code.
9
- * - `hooks <install|doctor|repair> <claude|codex> [--force]` → the matching
9
+ * - `hooks <install|doctor|repair> <claude|codex|opencode> [--force]` → the matching
10
10
  * agent command, exits with its code (unknown/missing agent → usage + exit 1).
11
11
  * Any other subcommand or a missing name → usage + exit 1.
12
12
  *
@@ -23,6 +23,8 @@ import { HOOKS_HELP_TEXT } from "../help.js";
23
23
  const AGENT_COMMAND_LOADERS = {
24
24
  claude: async () => (await import("../../commands/claude-hooks.js")).runClaudeHookCommand,
25
25
  codex: async () => (await import("../../commands/codex-hooks.js")).runCodexHookCommand,
26
+ opencode: async () => (await import("../../commands/opencode-hooks.js")).runOpenCodeHookCommand,
27
+ grok: async () => (await import("../../commands/grok-hooks.js")).runGrokHookCommand,
26
28
  };
27
29
  export async function handleHooks(cli) {
28
30
  if (cli.flags.help === true) {
@@ -45,7 +47,7 @@ export async function handleHooks(cli) {
45
47
  // Validity is decided by the agent registry (isAgentId), the single source
46
48
  // of truth; the loader map is exhaustive over AgentId by type.
47
49
  if (!isAgentId(agent)) {
48
- console.error(`Usage: javi-forge hooks ${sub} <claude|codex>`);
50
+ console.error(`Usage: javi-forge hooks ${sub} <claude|codex|opencode|grok>`);
49
51
  process.exit(1);
50
52
  }
51
53
  const run = await AGENT_COMMAND_LOADERS[agent]();
@@ -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|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";
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|opencode|grok> [--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 install opencode Install the managed OpenCode global plugin (~/.config/opencode/plugins/)\n doctor opencode Report OpenCode plugin and policy file currency (informational)\n repair opencode Repair the OpenCode plugin pair; --force overwrites edits\n install grok Install the Grok Build global PreToolUse hook (~/.grok/hooks/)\n doctor grok Report Grok hook registration and policy file currency (informational)\n repair grok Repair the Grok hook pair; --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 doctor opencode is informational and inspects installed files only; it does\n not prove OpenCode discovered, loaded, or executed the plugin.\n doctor grok is informational and inspects the global hook registration and\n adjacent policy runtime only; it does not prove Grok loaded or executed them.\n Linux: install/repair claude and grok need the acl package (getfacl) to\n prove the parent chain \u2014 apt install acl / apk add acl / dnf install acl.\n 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 $ javi-forge hooks install opencode\n $ javi-forge hooks doctor opencode\n $ javi-forge hooks repair opencode --force\n $ javi-forge hooks install grok\n $ javi-forge hooks doctor grok\n $ javi-forge hooks repair grok --force\n";
23
23
  export declare const FLAGS_SCHEMA: {
24
24
  readonly help: {
25
25
  readonly type: "boolean";
package/dist/cli/help.js CHANGED
@@ -167,7 +167,7 @@ export const CI_HELP_TEXT = `
167
167
  export const HOOKS_HELP_TEXT = `
168
168
  Usage
169
169
  $ javi-forge hooks run <pre-commit|pre-push>
170
- $ javi-forge hooks <install|doctor|repair> <claude|codex> [--force]
170
+ $ javi-forge hooks <install|doctor|repair> <claude|codex|opencode|grok> [--force]
171
171
 
172
172
  Run the sections enabled under hooks: in .javi-forge/ci.yaml, in a fixed
173
173
  cheap→expensive order, fail-fast. With no hooks: config the default is the
@@ -182,6 +182,12 @@ export const HOOKS_HELP_TEXT = `
182
182
  install codex Install the managed Codex PreToolUse guard (~/.codex/)
183
183
  doctor codex Report Codex hook execution readiness and trust boundary
184
184
  repair codex Repair the managed Codex guard; --force overwrites edits
185
+ install opencode Install the managed OpenCode global plugin (~/.config/opencode/plugins/)
186
+ doctor opencode Report OpenCode plugin and policy file currency (informational)
187
+ repair opencode Repair the OpenCode plugin pair; --force overwrites edits
188
+ install grok Install the Grok Build global PreToolUse hook (~/.grok/hooks/)
189
+ doctor grok Report Grok hook registration and policy file currency (informational)
190
+ repair grok Repair the Grok hook pair; --force overwrites edits
185
191
 
186
192
  Notes
187
193
  A blocking section failure exits non-zero and blocks the commit/push.
@@ -193,8 +199,13 @@ export const HOOKS_HELP_TEXT = `
193
199
  doctor codex exits 0 when runnable, 1 when blocked, and 2 when inconclusive;
194
200
  it does not prove provider trust or runtime execution. Use repair codex
195
201
  --force only to overwrite an edited managed asset.
196
- Linux: install/repair claude need the acl package (getfacl) to prove the
197
- parent chain — apt install acl / apk add acl / dnf install acl. Without it
202
+ doctor opencode is informational and inspects installed files only; it does
203
+ not prove OpenCode discovered, loaded, or executed the plugin.
204
+ doctor grok is informational and inspects the global hook registration and
205
+ adjacent policy runtime only; it does not prove Grok loaded or executed them.
206
+ Linux: install/repair claude and grok need the acl package (getfacl) to
207
+ prove the parent chain — apt install acl / apk add acl / dnf install acl.
208
+ Without it
198
209
  they refuse fail-closed; an already-installed guard keeps firing, and
199
210
  doctor claude reports the acl capability as its own row.
200
211
  Claude Code spawns the guard with node from ITS path, so node must resolve
@@ -209,6 +220,12 @@ export const HOOKS_HELP_TEXT = `
209
220
  $ javi-forge hooks install codex
210
221
  $ javi-forge hooks doctor codex
211
222
  $ javi-forge hooks repair codex --force
223
+ $ javi-forge hooks install opencode
224
+ $ javi-forge hooks doctor opencode
225
+ $ javi-forge hooks repair opencode --force
226
+ $ javi-forge hooks install grok
227
+ $ javi-forge hooks doctor grok
228
+ $ javi-forge hooks repair grok --force
212
229
  `;
213
230
  export const FLAGS_SCHEMA = {
214
231
  // `--help` is handled manually (autoHelp is disabled at the entrypoint so
@@ -0,0 +1,15 @@
1
+ /** Console renderer for the Grok Build global SkillGuard hook manager. */
2
+ import { doctorGrokSkillGuard, installGrokSkillGuard, repairGrokSkillGuard } from "../lib/grok-hook-manager.js";
3
+ export type GrokHookSub = "install" | "doctor" | "repair";
4
+ export interface GrokHookCmdDeps {
5
+ install?: typeof installGrokSkillGuard;
6
+ doctor?: typeof doctorGrokSkillGuard;
7
+ repair?: typeof repairGrokSkillGuard;
8
+ homeDir?: string;
9
+ log?: (msg: string) => void;
10
+ logError?: (msg: string) => void;
11
+ }
12
+ export declare function runGrokHookCommand(sub: GrokHookSub, _cwd: string, opts: {
13
+ force?: boolean;
14
+ }, deps?: GrokHookCmdDeps): Promise<number>;
15
+ //# sourceMappingURL=grok-hooks.d.ts.map
@@ -0,0 +1,45 @@
1
+ /** Console renderer for the Grok Build global SkillGuard hook manager. */
2
+ import { doctorGrokSkillGuard, installGrokSkillGuard, repairGrokSkillGuard, } from "../lib/grok-hook-manager.js";
3
+ function renderMutation(verb, result, log, logError) {
4
+ if (!result.ok) {
5
+ logError(`${verb} grok: refused`);
6
+ for (const error of result.errors)
7
+ logError(` ${error}`);
8
+ for (const backup of result.backups)
9
+ logError(` backup: ${backup}`);
10
+ return 1;
11
+ }
12
+ log(`${verb} grok: ok`);
13
+ if (result.changed.length === 0)
14
+ log("changed: nothing (already up to date)");
15
+ else
16
+ for (const changed of result.changed)
17
+ log(`changed: ${changed}`);
18
+ for (const backup of result.backups)
19
+ log(`backup: ${backup}`);
20
+ return 0;
21
+ }
22
+ function renderDoctor(report, log) {
23
+ log(`doctor grok: ${report.healthy ? "healthy" : "unhealthy"}`);
24
+ log(` hook: ${report.hook.state}`);
25
+ log(` policy: ${report.policy.state}`);
26
+ log(" runtime: not verified (installed files only)");
27
+ for (const remediation of report.remediation)
28
+ log(` remediation: ${remediation}`);
29
+ // This is intentionally informational: local files cannot prove that Grok
30
+ // discovered, loaded, or invoked a plugin in a real session.
31
+ return 0;
32
+ }
33
+ export async function runGrokHookCommand(sub, _cwd, opts, deps = {}) {
34
+ const log = deps.log ?? ((message) => console.log(message));
35
+ const logError = deps.logError ?? ((message) => console.error(message));
36
+ const home = deps.homeDir;
37
+ if (sub === "install")
38
+ return renderMutation("install", await (deps.install ?? installGrokSkillGuard)(home), log, logError);
39
+ if (sub === "repair")
40
+ return renderMutation("repair", await (deps.repair ?? repairGrokSkillGuard)(home, {
41
+ force: opts.force === true,
42
+ }), log, logError);
43
+ return renderDoctor(await (deps.doctor ?? doctorGrokSkillGuard)(home), log);
44
+ }
45
+ //# sourceMappingURL=grok-hooks.js.map
@@ -0,0 +1,15 @@
1
+ /** Console renderer for the OpenCode SkillGuard global plugin manager. */
2
+ import { doctorOpenCodeSkillGuard, installOpenCodeSkillGuard, repairOpenCodeSkillGuard } from "../lib/opencode-hook-manager.js";
3
+ export type OpenCodeHookSub = "install" | "doctor" | "repair";
4
+ export interface OpenCodeHookCmdDeps {
5
+ install?: typeof installOpenCodeSkillGuard;
6
+ doctor?: typeof doctorOpenCodeSkillGuard;
7
+ repair?: typeof repairOpenCodeSkillGuard;
8
+ homeDir?: string;
9
+ log?: (msg: string) => void;
10
+ logError?: (msg: string) => void;
11
+ }
12
+ export declare function runOpenCodeHookCommand(sub: OpenCodeHookSub, _cwd: string, opts: {
13
+ force?: boolean;
14
+ }, deps?: OpenCodeHookCmdDeps): Promise<number>;
15
+ //# sourceMappingURL=opencode-hooks.d.ts.map
@@ -0,0 +1,41 @@
1
+ /** Console renderer for the OpenCode SkillGuard global plugin manager. */
2
+ import { doctorOpenCodeSkillGuard, installOpenCodeSkillGuard, repairOpenCodeSkillGuard, } from "../lib/opencode-hook-manager.js";
3
+ function renderMutation(verb, result, log, logError) {
4
+ if (!result.ok) {
5
+ logError(`${verb} opencode: refused`);
6
+ for (const error of result.errors)
7
+ logError(` ${error}`);
8
+ return 1;
9
+ }
10
+ log(`${verb} opencode: ok`);
11
+ if (result.changed.length === 0)
12
+ log("changed: nothing (already up to date)");
13
+ else
14
+ for (const changed of result.changed)
15
+ log(`changed: ${changed}`);
16
+ return 0;
17
+ }
18
+ function renderDoctor(report, log) {
19
+ log(`doctor opencode: ${report.healthy ? "healthy" : "unhealthy"}`);
20
+ log(` plugin: ${report.plugin.state}`);
21
+ log(` policy: ${report.policy.state}`);
22
+ log(" runtime: not verified (installed files only)");
23
+ for (const remediation of report.remediation)
24
+ log(` remediation: ${remediation}`);
25
+ // This is intentionally informational: local files cannot prove that OpenCode
26
+ // discovered, loaded, or invoked a plugin in a real session.
27
+ return 0;
28
+ }
29
+ export async function runOpenCodeHookCommand(sub, _cwd, opts, deps = {}) {
30
+ const log = deps.log ?? ((message) => console.log(message));
31
+ const logError = deps.logError ?? ((message) => console.error(message));
32
+ const home = deps.homeDir;
33
+ if (sub === "install")
34
+ return renderMutation("install", await (deps.install ?? installOpenCodeSkillGuard)(home), log, logError);
35
+ if (sub === "repair")
36
+ return renderMutation("repair", await (deps.repair ?? repairOpenCodeSkillGuard)(home, {
37
+ force: opts.force === true,
38
+ }), log, logError);
39
+ return renderDoctor(await (deps.doctor ?? doctorOpenCodeSkillGuard)(home), log);
40
+ }
41
+ //# sourceMappingURL=opencode-hooks.js.map
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Agent adapter registry (agent-agnostic slice 2). One descriptor per host
3
- * (`claude`, `codex`) capturing the per-agent facts the SkillGuard installer and
3
+ * (`claude`, `codex`, `opencode`) capturing the per-agent facts the SkillGuard installer and
4
4
  * doctor need: config paths, the protected managed set, the project-root source,
5
- * the settings-schema validator (SHARED — both hosts use the identical hooks
6
- * schema), the managed marker, the deny protocol, and the trust model.
5
+ * the host registration schema, the managed marker, the deny protocol, and the
6
+ * trust model.
7
7
  *
8
8
  * This descriptor is additive: Codex is routed through it, while Claude keeps its
9
9
  * existing, byte-identical runtime path (the descriptor only DESCRIBES Claude; it
@@ -14,7 +14,7 @@
14
14
  * map is typed `Record<AgentId, …>`, so omitting a loader is a compile error.
15
15
  */
16
16
  import { validateSettingsShape } from "./claude-hook-settings.js";
17
- export type AgentId = "claude" | "codex";
17
+ export type AgentId = "claude" | "codex" | "opencode" | "grok";
18
18
  export type TrustState = "trusted" | "untrusted" | "unknown";
19
19
  export interface TrustDescriptor {
20
20
  /** Detect whether the installed hook is TRUSTED from the host config text. */
@@ -35,17 +35,19 @@ export interface AgentAdapter {
35
35
  projectDir: {
36
36
  envVar: string | null;
37
37
  };
38
- /** SHARED settings-schema validator — the hooks container shape is identical. */
39
- settingsSchema: typeof validateSettingsShape;
38
+ /** Host settings-schema validator, or null when the host does not use one. */
39
+ settingsSchema: typeof validateSettingsShape | null;
40
40
  /** The managed asset marker for this host. */
41
41
  marker: string;
42
- /** Deny protocol emitted by the shared `.mjs` — the same on both hosts. */
43
- emitDeny: "exit2+stderr";
42
+ /** Deny protocol emitted by the host integration. */
43
+ emitDeny: "exit2+stderr" | "throw-error";
44
44
  /** Hook-trust model, or null when the host has none (Claude). */
45
45
  trust: TrustDescriptor | null;
46
46
  }
47
47
  export declare const claudeAdapter: AgentAdapter;
48
48
  export declare const codexAdapter: AgentAdapter;
49
+ export declare const opencodeAdapter: AgentAdapter;
50
+ export declare const grokAdapter: AgentAdapter;
49
51
  export declare const AGENT_ADAPTERS: Record<AgentId, AgentAdapter>;
50
52
  /**
51
53
  * Whether a raw CLI token names a known agent adapter. Backed by the