ohmyjev 0.6.0 → 0.7.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Novacon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -13,14 +13,14 @@ d88P 88888888 "Y88P" 888 888 888 888 888 "Y88888 888 "Y8888 Y88P
13
13
  ```
14
14
 
15
15
  <p align="center">
16
- Guardrails for Claude Code, pi and omp, decided by <a href="https://typesafe.ai">Jev</a>.<br>
16
+ Guardrails for Claude Code, Codex, pi and omp, decided by <a href="https://typesafe.ai">Jev</a>.<br>
17
17
  One mod that stops irreversible and data-leaking commands, even in bypass mode, and runs them when you say yes. It also flags prompt injection, pushes back on an unverified "done" and routes effort per turn.
18
18
  </p>
19
19
 
20
20
  <p align="center">
21
21
  <img alt="Claude Code 2.1.287+" src="https://img.shields.io/badge/Claude_Code-2.1.287%2B-d97757">
22
22
  <img alt="Decided by Jev" src="https://img.shields.io/badge/decided_by-Jev-101315">
23
- <img alt="pi and omp" src="https://img.shields.io/badge/also_for-pi_·_omp-86a893">
23
+ <img alt="Codex, pi and omp" src="https://img.shields.io/badge/also_for-Codex_·_pi_·_omp-86a893">
24
24
  <a href="https://www.npmjs.com/package/ohmyjev"><img alt="npm" src="https://img.shields.io/npm/v/ohmyjev?color=de6145"></a>
25
25
  <img alt="MIT license" src="https://img.shields.io/badge/license-MIT-798186">
26
26
  </p>
@@ -30,7 +30,7 @@ d88P 88888888 "Y88P" 888 888 888 888 888 "Y88888 888 "Y8888 Y88P
30
30
  <a href="#quick-start">Quick start</a> ·
31
31
  <a href="#what-you-get">What you get</a> ·
32
32
  <a href="#install">Install</a> ·
33
- <a href="#pi-and-omp">pi and omp</a> ·
33
+ <a href="#codex-pi-and-omp">Codex, pi and omp</a> ·
34
34
  <a href="#using-it">Using it</a> ·
35
35
  <a href="#settings">Settings</a> ·
36
36
  <a href="#privacy">Privacy</a> ·
@@ -144,11 +144,29 @@ key: env TYPESAFE_API_KEY · typesafe · jev-1.13.0
144
144
  If it says `key: none`, ohmyjev can't see a key. In that case every gate stays open, the router falls back to Claude
145
145
  Code's built-in classifier (up only), and the status under the prompt shows `jev ⚠ no key`.
146
146
 
147
- ## pi and omp
147
+ ## Codex, pi and omp
148
148
 
149
- The same checks run in [pi](https://pi.dev) and [omp](https://github.com/can1357/oh-my-pi) as a native extension.
150
- They share the decision logic and the key lookup (`TYPESAFE_API_KEY`, then `OPENROUTER_API_KEY`) with the Claude Code
151
- plugin, and write to the same `~/.ohmyjev` logs, so `/omj` and the statusline segment work the same way.
149
+ The same checks run in [Codex](https://developers.openai.com/codex) as a plugin with hooks, and in [pi](https://pi.dev)
150
+ and [omp](https://github.com/can1357/oh-my-pi) as a native extension. They share the decision logic and the key lookup
151
+ (`TYPESAFE_API_KEY`, then `OPENROUTER_API_KEY`) with the Claude Code plugin, and write to the same `~/.ohmyjev` logs, so
152
+ `/omj stats` in any of them shows every session.
153
+
154
+ ### Codex
155
+
156
+ ```bash
157
+ codex plugin marketplace add Novacon/ohmyjev
158
+ codex plugin add ohmyjev@ohmyjev
159
+ ```
160
+
161
+ Then start Codex and run `/hooks` once: Codex skips a plugin's hooks until you trust them. ohmyjev needs Node on your
162
+ `PATH` and the key in the shell that starts Codex. It gates `Bash`, `apply_patch` edits and MCP calls, screens their
163
+ output, and runs the done-check. Saying yes after a block works the same way. Codex hooks can't change the model or add
164
+ a tool, so there is no router, no `ask_jev` and no `/omj` command. Put settings in `~/.codex/ohmyjev.json`, using the
165
+ names in [Settings](#settings):
166
+
167
+ ```json
168
+ { "requested": 0.9, "policies": "never touch the prod cluster" }
169
+ ```
152
170
 
153
171
  ### omp
154
172
 
@@ -184,18 +202,19 @@ pi has no settings screen for extensions, so put yours under an `ohmyjev` key in
184
202
 
185
203
  ### What differs
186
204
 
187
- | | Claude Code | omp | pi |
188
- |---|---|---|---|
189
- | Bash gate | Bash | `bash`, `eval`, and `github` pushes and PRs | `bash`, `powershell` |
190
- | Write gate | Write, Edit, NotebookEdit | `write`, `edit` (patches and renames included), `ast_edit` | `write`, `edit` |
191
- | Exfil gate | WebFetch, MCP tools | `read` of a URL, MCP tools | MCP tools (pi has no fetch tool; `curl` goes through the bash gate) |
192
- | Injection screen | Bash, WebFetch, MCP, Reads outside the repo | `bash`, `github`, URL reads, MCP, reads outside the repo | `bash`, `powershell`, MCP, reads outside the repo |
193
- | Done-check | ✓ | ✓ | ✓ |
194
- | Router effort | ✓ | ✓ (left alone while you're on `auto`) | ✓ |
195
- | Router subagent model | general-purpose subagents | subagents on the default `task` role | none: pi has no subagents |
196
- | Router without a key | Claude Code's built-in classifier, up only | off: omp has no separate classifier call for extensions | the cheapest model you've set up, up only |
197
- | Decision lines | in the transcript | notifications | notifications |
198
- | Status | under the prompt | in the footer | in the footer |
205
+ | | Claude Code | Codex | omp | pi |
206
+ |---|---|---|---|---|
207
+ | Bash gate | Bash | Bash | `bash`, `eval`, and `github` pushes and PRs | `bash`, `powershell` |
208
+ | Write gate | Write, Edit, NotebookEdit | `apply_patch` | `write`, `edit` (patches and renames included), `ast_edit` | `write`, `edit` |
209
+ | Exfil gate | WebFetch, MCP tools | MCP tools | `read` of a URL, MCP tools | MCP tools (pi has no fetch tool; `curl` goes through the bash gate) |
210
+ | Injection screen | Bash, WebFetch, MCP, Reads outside the repo | Bash, MCP | `bash`, `github`, URL reads, MCP, reads outside the repo | `bash`, `powershell`, MCP, reads outside the repo |
211
+ | Done-check | ✓ | ✓ | ✓ | ✓ |
212
+ | Router effort | ✓ | none: hooks can't set it | ✓ (left alone while you're on `auto`) | ✓ |
213
+ | Router subagent model | general-purpose subagents | none | subagents on the default `task` role | none: pi has no subagents |
214
+ | Router without a key | Claude Code's built-in classifier, up only | none | off: omp has no separate classifier call for extensions | the cheapest model you've set up, up only |
215
+ | `/omj`, `ask_jev` | ✓ | none: hooks can't add commands or tools | ✓ | ✓ |
216
+ | Decision lines | in the transcript | the log only | notifications | notifications |
217
+ | Status | under the prompt | none | in the footer | in the footer |
199
218
 
200
219
  ## Using it
201
220
 
@@ -379,6 +398,9 @@ have set up, through pi's own provider. Logs and session files stay in `~/.ohmyj
379
398
  claude plugin update ohmyjev@ohmyjev # then restart Claude Code
380
399
  claude plugin uninstall ohmyjev@ohmyjev
381
400
 
401
+ codex plugin marketplace upgrade ohmyjev # Codex; then trust the changed hooks in /hooks
402
+ codex plugin remove ohmyjev@ohmyjev
403
+
382
404
  omp plugin upgrade ohmyjev # omp; restart the session
383
405
  omp plugin uninstall ohmyjev
384
406
 
@@ -0,0 +1,163 @@
1
+ /**
2
+ * ohmyjev for Codex: one command hook, run once per event (codex/hooks.json). Each run is a fresh process, so what
3
+ * must outlive it (the user's latest request, the call blocked last turn, this turn's tools) sits in
4
+ * ~/.ohmyjev/codex/<session>.json. Same decisions as the other hosts (hooks/policy.ts); every failure prints nothing,
5
+ * and nothing lets the call through. Codex hooks can't change the model or add tools, so there is no router or ask_jev.
6
+ * Built to codex/ohmyjev.mjs with `bun run build:codex`, since Codex runs it with plain node.
7
+ */
8
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
9
+ import {
10
+ BASH_Q, EXFIL_Q, SCREEN_Q, STOP_Q, WRITE_Q, applyPatchPaths, callSummary, clip, denyText, gateBash, gateExfil, gateWrite,
11
+ judgeStop, pathDenyText, plainRead, readConfig, sanitizeSid, screen, withPolicies, withPolicyQ, type Config,
12
+ } from '../hooks/policy.ts'
13
+ import { Session, keyOf, pathAllowed, repoRoot, type Env, type Fetch } from './shared.ts'
14
+
15
+ const CLIP = 16000
16
+ type Tool = { tool: string; input: string; outcome: 'ok' | 'failed' }
17
+ type State = { request: string; previous: string[]; count: number; blockedBefore: string; lastBlocked: string; tools: Tool[]; pushedBackAt: number }
18
+ const EMPTY: State = { request: '', previous: [], count: 0, blockedBefore: '', lastBlocked: '', tools: [], pushedBackAt: -1 }
19
+ type Input = Record<string, unknown>
20
+ const str = (o: Input, k: string): string => (typeof o[k] === 'string' ? (o[k] as string) : '')
21
+ const obj = (v: unknown): Input => (v && typeof v === 'object' && !Array.isArray(v) ? (v as Input) : {})
22
+
23
+ /** Settings: ~/.codex/ohmyjev.json, a flat object of the setting names in the README. */
24
+ async function config(env: Env): Promise<Config> {
25
+ const text = await readFile(`${env.HOME ?? ''}/.codex/ohmyjev.json`, 'utf8').catch(() => '{}')
26
+ try {
27
+ return readConfig(obj(JSON.parse(text)))
28
+ } catch {
29
+ return readConfig({})
30
+ }
31
+ }
32
+
33
+ const statePath = (env: Env, sid: string) => `${env.HOME ?? ''}/.ohmyjev/codex/${sanitizeSid(sid)}.json`
34
+ async function load(env: Env, sid: string): Promise<State> {
35
+ const text = await readFile(statePath(env, sid), 'utf8').catch(() => '')
36
+ try {
37
+ return { ...EMPTY, ...(JSON.parse(text) as Partial<State>) }
38
+ } catch {
39
+ return { ...EMPTY }
40
+ }
41
+ }
42
+ // ponytail: last write wins; two hooks finishing at once can drop one tool from this turn's list (done-check input only)
43
+ async function save(env: Env, sid: string, st: State): Promise<void> {
44
+ await mkdir(`${env.HOME ?? ''}/.ohmyjev/codex`, { recursive: true, mode: 0o700 }).catch(() => undefined)
45
+ await writeFile(statePath(env, sid), JSON.stringify(st), { mode: 0o600 }).catch(() => undefined)
46
+ }
47
+
48
+ const deny = (reason: string) => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } })
49
+
50
+ /** One hook event in, Codex's JSON answer out; undefined means no opinion. */
51
+ export async function handle(input: Input, env: Env, fetchImpl?: Fetch): Promise<object | undefined> {
52
+ const c = await config(env)
53
+ const event = str(input, 'hook_event_name')
54
+ const sid = str(input, 'session_id') || 'codex'
55
+ const cwd = str(input, 'cwd') || process.cwd()
56
+ if (!c.enabled) return undefined
57
+
58
+ if (event === 'SessionStart')
59
+ return keyOf(c, env) ? undefined : { systemMessage: 'ohmyjev: no Jev key, so every call passes through. Set TYPESAFE_API_KEY in the shell that starts Codex.' }
60
+
61
+ const st = await load(env, sid)
62
+ if (event === 'UserPromptSubmit') {
63
+ const prompt = str(input, 'prompt')
64
+ if (!prompt.trim() || /^\/\S+\s*$/.test(prompt.trim())) return undefined
65
+ if (st.request) st.previous = [...st.previous, clip(st.request, 200)].slice(-5)
66
+ Object.assign(st, { request: prompt, count: st.count + 1, blockedBefore: st.lastBlocked, lastBlocked: '', tools: [] })
67
+ await save(env, sid, st)
68
+ return undefined
69
+ }
70
+
71
+ const x = new Session(c, env, sid, undefined, fetchImpl)
72
+ const tool = str(input, 'tool_name')
73
+ const toolInput = obj(input.tool_input)
74
+
75
+ if (event === 'PreToolUse') {
76
+ const blocked = async (j: { verdict: unknown; reason: string }) => {
77
+ if (j.verdict !== 'deny') return undefined
78
+ st.lastBlocked = callSummary(tool, toolInput)
79
+ await save(env, sid, st)
80
+ return deny(denyText(j.reason))
81
+ }
82
+ if (tool === 'Bash' && c.bashGate) {
83
+ const command = str(toolInput, 'command')
84
+ if (plainRead(command, c)) return undefined
85
+ const state = withPolicies({ command: clip(command, CLIP), cwd, ...(command.length > CLIP ? { truncated: true } : {}) }, c, st.request, st.blockedBefore)
86
+ const d = await x.decide('tool.call', tool, state, withPolicyQ(BASH_Q, c, st.request))
87
+ if (!d) return undefined
88
+ const j = gateBash(d.answers, c)
89
+ x.record(d, j.verdict, j.reason)
90
+ return blocked(j)
91
+ }
92
+ if (tool === 'apply_patch' && c.writeGate) {
93
+ const patch = str(toolInput, 'command')
94
+ const root = await repoRoot(cwd)
95
+ for (const path of applyPatchPaths(patch))
96
+ if (!(await pathAllowed(path, cwd, root, c, env))) {
97
+ x.denyByCode(tool, `${path} is outside the repo and allowPaths`)
98
+ return deny(pathDenyText(path))
99
+ }
100
+ const state = withPolicies({ path: applyPatchPaths(patch).join('; '), content: clip(patch, CLIP) }, c, st.request, st.blockedBefore)
101
+ const d = await x.decide('tool.call', tool, state, withPolicyQ(WRITE_Q, c, st.request))
102
+ if (!d) return undefined
103
+ const j = gateWrite(d.answers, c)
104
+ x.record(d, j.verdict, j.reason)
105
+ return blocked(j)
106
+ }
107
+ if (tool.startsWith('mcp__') && c.exfilGate) {
108
+ const state = withPolicies({ tool, input: clip(JSON.stringify(toolInput), 4000) }, c, st.request, st.blockedBefore)
109
+ const d = await x.decide('tool.call', tool, state, withPolicyQ(EXFIL_Q, c, st.request))
110
+ if (!d) return undefined
111
+ const j = gateExfil(d.answers, c)
112
+ x.record(d, j.verdict, j.reason)
113
+ return blocked(j)
114
+ }
115
+ return undefined
116
+ }
117
+
118
+ if (event === 'PostToolUse') {
119
+ const response = input.tool_response
120
+ const code = obj(response).exit_code ?? obj(response).exitCode
121
+ st.tools = [...st.tools, { tool, input: clip(JSON.stringify(toolInput), 200), outcome: typeof code === 'number' && code !== 0 ? 'failed' : 'ok' } as Tool].slice(-20)
122
+ await save(env, sid, st)
123
+ if (!c.injectionScreen || !(tool === 'Bash' || tool.startsWith('mcp__'))) return undefined
124
+ const text = typeof response === 'string' ? response : JSON.stringify(response ?? '')
125
+ if (!text.trim()) return undefined
126
+ const d = await x.decide('tool.result', tool, { tool, content: clip(text, 6000) }, SCREEN_Q)
127
+ if (!d) return undefined
128
+ const s = screen(d.answers, c)
129
+ x.record(d, s.flagged ? 'flag' : null, s.reason)
130
+ return s.flagged ? { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: s.note } } : undefined
131
+ }
132
+
133
+ if (event === 'Stop') {
134
+ if (!c.doneCheck || input.stop_hook_active === true || st.pushedBackAt === st.count) return undefined
135
+ const state = {
136
+ current_request: clip(st.request, 600),
137
+ previous_requests: st.previous,
138
+ tools_this_turn: st.tools,
139
+ last_assistant_message: clip(str(input, 'last_assistant_message'), 1500),
140
+ }
141
+ const d = await x.decide('Stop', '', state, STOP_Q)
142
+ if (!d) return undefined
143
+ const j = judgeStop(d.answers, c, st.tools.length > 0)
144
+ x.record(d, j.block ? 'block' : null, j.block ?? 'stop ok')
145
+ if (!j.block) return undefined
146
+ st.pushedBackAt = st.count
147
+ await save(env, sid, st)
148
+ return { decision: 'block', reason: j.block }
149
+ }
150
+ return undefined
151
+ }
152
+
153
+ /** The hook command: one JSON event on stdin, the answer on stdout. Any failure prints nothing, so Codex goes on. */
154
+ export async function main(): Promise<void> {
155
+ try {
156
+ let text = ''
157
+ for await (const chunk of process.stdin) text += chunk
158
+ const out = await handle(obj(JSON.parse(text)), process.env)
159
+ if (out) process.stdout.write(JSON.stringify(out))
160
+ } catch {
161
+ // ohmyjev's own bug: no opinion
162
+ }
163
+ }
package/adapters/omp.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFile } from 'node:fs/promises'
5
5
  import {
6
6
  BASH_Q, DEFAULTS, EXFIL_Q, ROUTE_Q, SCREEN_Q, STOP_Q, SWITCHED_Q, WRITE_Q,
7
- clip, decideRoute, denyText, gateBash, gateExfil, gateWrite, jevRouteLine, judgeStop, pathDenyText, callSummary, plainRead, configCommand,
7
+ clip, decideRoute, denyText, gateBash, gateExfil, gateWrite, jevRouteLine, judgeStop, pathDenyText, callSummary, plainRead, configCommand, applyPatchPaths,
8
8
  OMJ_HELP, OMJ_OFF, OMJ_ON, toggleArg,
9
9
  keepInstructions, modelOf, readConfig, routeStep, screen, stepLine, withPolicies, withPolicyQ,
10
10
  type Config, type Route,
@@ -150,15 +150,6 @@ const exfilTool = (tool: string, input: RecordOfUnknown): boolean => urlRead(too
150
150
  const githubWrite = (tool: string, input: RecordOfUnknown): boolean =>
151
151
  tool === 'github' && GITHUB_READONLY_OPS[string(input, 'op')] !== true
152
152
 
153
- function applyPatchPaths(patch: string): string[] {
154
- const paths: string[] = []
155
- for (const line of patch.split('\n')) {
156
- const match = /^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/.exec(line.trim())
157
- if (match?.[1]) paths.push(match[1])
158
- }
159
- return paths
160
- }
161
-
162
153
  function writePaths(tool: string, input: RecordOfUnknown): string[] {
163
154
  const paths = Array.isArray(input.paths) ? input.paths.filter((p): p is string => typeof p === 'string' && p !== '') : []
164
155
  for (const key of ['path', 'file_path', 'notebook_path']) {
package/hooks/policy.ts CHANGED
@@ -480,6 +480,16 @@ export function hostPath(p: string): string | null {
480
480
  export const parseWorktrees = (porcelain: string): string[] =>
481
481
  porcelain.split('\n').filter(l => l.startsWith('worktree ')).map(l => l.slice('worktree '.length).trim()).filter(Boolean)
482
482
 
483
+ /** Every file an apply_patch patch adds, updates, deletes or moves to (omp and Codex edits). */
484
+ export function applyPatchPaths(patch: string): string[] {
485
+ const paths: string[] = []
486
+ for (const line of patch.split('\n')) {
487
+ const match = /^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/.exec(line.trim())
488
+ if (match?.[1]) paths.push(match[1])
489
+ }
490
+ return paths
491
+ }
492
+
483
493
  export const isUnder = (target: string, root: string): boolean =>
484
494
  target === root || target.startsWith(root.replace(/\/+$/, '') + '/')
485
495
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohmyjev",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Guardrails for coding agents, decided by TypeSafe's Jev: blocks destructive commands, catches prompt injection, pushes back on an unverified done and routes effort per turn. For Claude Code, pi and omp.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,8 +14,18 @@
14
14
  "omp",
15
15
  "pi",
16
16
  "claude-code",
17
+ "claude-code-plugin",
18
+ "codex",
19
+ "codex-plugin",
17
20
  "jev",
18
- "guardrails"
21
+ "guardrails",
22
+ "ai-agent",
23
+ "ai-safety",
24
+ "llm-security",
25
+ "prompt-injection",
26
+ "coding-agent",
27
+ "agent-guardrails",
28
+ "bypass-permissions"
19
29
  ],
20
30
  "files": [
21
31
  "adapters/*.ts",
@@ -235,7 +245,8 @@
235
245
  },
236
246
  "scripts": {
237
247
  "test": "bun test adapters && claude plugin test .",
238
- "typecheck": "tsc -p . && tsc -p tsconfig.adapters.json"
248
+ "typecheck": "tsc -p . && tsc -p tsconfig.adapters.json",
249
+ "build:codex": "bun build codex/main.ts --target=node --format=esm --outfile=codex/ohmyjev.mjs"
239
250
  },
240
251
  "devDependencies": {
241
252
  "@types/bun": "^1.4.2",