openqodex 0.1.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.
Files changed (90) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +35 -0
  3. package/README.md +160 -0
  4. package/demo/baseline/.github/workflows/ci.yml +18 -0
  5. package/demo/baseline/Dockerfile +12 -0
  6. package/demo/baseline/app/__init__.py +0 -0
  7. package/demo/baseline/app/server.py +37 -0
  8. package/demo/baseline/package-lock.json +21 -0
  9. package/demo/baseline/package.json +9 -0
  10. package/demo/baseline/requirements.txt +1 -0
  11. package/demo/baseline/scripts/deploy.sh +13 -0
  12. package/demo/expected.json +126 -0
  13. package/demo/planted/.github/workflows/ci.yml +20 -0
  14. package/demo/planted/Dockerfile +11 -0
  15. package/demo/planted/app/config.py +3 -0
  16. package/demo/planted/app/search.py +17 -0
  17. package/demo/planted/app/server.py +40 -0
  18. package/demo/planted/package-lock.json +21 -0
  19. package/demo/planted/package.json +9 -0
  20. package/demo/planted/scripts/deploy.sh +13 -0
  21. package/dist/bin.js +42546 -0
  22. package/docs/agents.md +121 -0
  23. package/docs/cli.md +161 -0
  24. package/docs/config.md +165 -0
  25. package/docs/custom-scanners.md +164 -0
  26. package/docs/faq.md +54 -0
  27. package/docs/github-action.md +73 -0
  28. package/docs/index.md +29 -0
  29. package/docs/llms.txt +13 -0
  30. package/docs/quickstart.md +82 -0
  31. package/docs/scanners.md +148 -0
  32. package/docs/security.md +74 -0
  33. package/docs/telemetry.md +9 -0
  34. package/lenses/a11y-icon-only-button-no-aria-label.md +51 -0
  35. package/lenses/array-iteration-missing-key-prop.md +31 -0
  36. package/lenses/async-await-in-loop-n-plus-one.md +42 -0
  37. package/lenses/async-click-double-fire-race.md +59 -0
  38. package/lenses/async-floating-promise.md +40 -0
  39. package/lenses/async-promise-all-swallows-errors.md +42 -0
  40. package/lenses/async-unhandled-rejection-in-handler.md +42 -0
  41. package/lenses/auth-missing-on-state-change-route.md +60 -0
  42. package/lenses/auth-role-from-user-input.md +49 -0
  43. package/lenses/auth-timing-attack-password-compare.md +59 -0
  44. package/lenses/cookie-missing-secure-httponly.md +46 -0
  45. package/lenses/cors-wildcard-with-credentials.md +47 -0
  46. package/lenses/crypto-jwt-verify-without-algo-allowlist.md +41 -0
  47. package/lenses/crypto-math-random-for-tokens.md +53 -0
  48. package/lenses/crypto-md5-sha1-for-secrets.md +60 -0
  49. package/lenses/env-vars-read-at-module-top.md +42 -0
  50. package/lenses/eval-on-user-input.md +51 -0
  51. package/lenses/fetch-without-timeout.md +41 -0
  52. package/lenses/id-enumeration-sequential.md +49 -0
  53. package/lenses/interactive-state-decoupled-from-output.md +67 -0
  54. package/lenses/json-parse-no-try-catch.md +41 -0
  55. package/lenses/missing-rate-limit-on-auth.md +49 -0
  56. package/lenses/oauth-scope-wider-than-use.md +79 -0
  57. package/lenses/object-spread-clobber.md +44 -0
  58. package/lenses/open-redirect-from-untrusted-host.md +55 -0
  59. package/lenses/orm-drizzle-on-conflict-clobber.md +47 -0
  60. package/lenses/path-traversal-in-fs-access.md +59 -0
  61. package/lenses/pii-in-url-or-log.md +48 -0
  62. package/lenses/race-check-then-act.md +49 -0
  63. package/lenses/react-dangerously-set-inner-html.md +32 -0
  64. package/lenses/react-fetch-in-effect-without-abort.md +31 -0
  65. package/lenses/react-stale-closure-in-callback.md +28 -0
  66. package/lenses/react-state-set-in-render.md +33 -0
  67. package/lenses/react-use-effect-missing-cleanup.md +29 -0
  68. package/lenses/react-use-effect-missing-deps.md +30 -0
  69. package/lenses/regexp-from-user-input.md +43 -0
  70. package/lenses/return-shape-contract-break.md +71 -0
  71. package/lenses/secrets-logged-in-error-path.md +60 -0
  72. package/lenses/sql-migration-references-later-object.md +47 -0
  73. package/lenses/sql-string-concatenation.md +63 -0
  74. package/lenses/ssrf-server-side-fetch.md +69 -0
  75. package/lenses/supabase-comment-on-function-unqualified.md +37 -0
  76. package/lenses/supabase-function-default-public-execute.md +50 -0
  77. package/lenses/supabase-security-definer-no-search-path.md +34 -0
  78. package/lenses/supabase-single-500-on-no-match.md +65 -0
  79. package/lenses/upsert-state-column.md +57 -0
  80. package/lenses/url-not-encoded-for-user-id.md +78 -0
  81. package/lenses/use-state-default-not-functional.md +30 -0
  82. package/package.json +57 -0
  83. package/skills/openqodex/SKILL.md +141 -0
  84. package/templates/README.md +67 -0
  85. package/templates/claude-code/settings-hook.json +16 -0
  86. package/templates/cline/openqodex.md +9 -0
  87. package/templates/codex/AGENTS-section.md +8 -0
  88. package/templates/codex/hooks.json +16 -0
  89. package/templates/cursor/openqodex.mdc +15 -0
  90. package/toolchain.json +345 -0
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: upsert-state-column
3
+ description: Upsert / on-conflict writes that clobber workflow-state columns
4
+ triggers:
5
+ files:
6
+ - "*.ts"
7
+ - "*.tsx"
8
+ - "*.js"
9
+ - "*.jsx"
10
+ - "*.mjs"
11
+ - "*.cjs"
12
+ - "*.py"
13
+ - "*.go"
14
+ - "*.rb"
15
+ - "*.java"
16
+ - "*.kt"
17
+ - "*.cs"
18
+ - "*.php"
19
+ - "*.sql"
20
+ - "**/*.ts"
21
+ - "**/*.tsx"
22
+ - "**/*.js"
23
+ - "**/*.jsx"
24
+ - "**/*.mjs"
25
+ - "**/*.cjs"
26
+ - "**/*.py"
27
+ - "**/*.go"
28
+ - "**/*.rb"
29
+ - "**/*.java"
30
+ - "**/*.kt"
31
+ - "**/*.cs"
32
+ - "**/*.php"
33
+ - "**/*.sql"
34
+ hunk_regex: "\\b(upsert|on[\\s_-]?conflict|onconflict|merge\\s+into)\\b"
35
+ confidence_floor: 0.7
36
+ ---
37
+
38
+ An upsert / on-conflict insert that writes a literal value to a
39
+ state-bearing column (stage, status, state, tier, role, phase, plan)
40
+ will overwrite that column on every matching row, including rows that
41
+ have already advanced past that state. On CRM, billing, fulfillment,
42
+ and auth flows this silently regresses the workflow on the next
43
+ webhook or replay.
44
+
45
+ Flag as a bug **unless** the write is explicitly guarded:
46
+
47
+ - `ignoreDuplicates: true` / `ON CONFLICT DO NOTHING`
48
+ - an `updateColumns` (drizzle / kysely / supabase) or `DO UPDATE SET`
49
+ list that **omits** the state column
50
+ - a `WHERE` / `.eq()` predicate on the upsert that constrains by the
51
+ current value of the state column
52
+
53
+ Confidence rule still applies: if you cannot tell from the diff
54
+ whether such a guard exists upstream (helper wrapping the upsert,
55
+ trigger on the table, generated-column default), read the surrounding
56
+ code before raising. Drop the finding when the guard
57
+ is plausible and you can't disprove it.
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: url-not-encoded-for-user-id
3
+ description: URL string built by concatenating a user-supplied identifier without encodeURIComponent
4
+ triggers:
5
+ files:
6
+ - "*.ts"
7
+ - "*.tsx"
8
+ - "*.js"
9
+ - "*.jsx"
10
+ - "*.mjs"
11
+ - "*.cjs"
12
+ - "**/*.ts"
13
+ - "**/*.tsx"
14
+ - "**/*.js"
15
+ - "**/*.jsx"
16
+ - "**/*.mjs"
17
+ - "**/*.cjs"
18
+ hunk_regex: "https?://|`/[a-z]|/api/|/users?/|/teams?/|/projects?/|\\?[a-z_]+="
19
+ security: true
20
+ confidence_floor: 0.8
21
+ ---
22
+
23
+ A URL is built by string concatenation or template-string
24
+ interpolation of an identifier (uuid, slug, user id, email,
25
+ search query, encoded token) without `encodeURIComponent`. When
26
+ the substituted value contains a reserved character (`#`, `?`,
27
+ `&`, `/`, `=`, `+`, space) the URL silently breaks: the trailing
28
+ path becomes a fragment, query params merge into the previous one,
29
+ or the slash splits the route. Worst case it's a one-character
30
+ SSRF if the substituted value can contain `@` (rewrites the host)
31
+ or a `..` segment that escapes the intended subdirectory.
32
+
33
+ Flag when a URL string in this diff:
34
+ - is built with `+ var`, `${var}`, or `concat(var)` where `var` is
35
+ named like an identifier (`id`, `userId`, `slug`, `email`,
36
+ `token`, `name`, `query`, `searchTerm`, `*_id`, `*_uuid`),
37
+ - AND the substitution happens in a path segment or query value
38
+ position,
39
+ - AND there is no `encodeURIComponent(var)` / `encodeURI(var)` /
40
+ `new URL(...).searchParams.set(...)` / equivalent encode call
41
+ on the way in,
42
+ - AND `var` is not a hardcoded constant from earlier in the
43
+ function.
44
+
45
+ Examples that should fire:
46
+ - `` `${baseUrl}/users/${userId}` `` where userId can hold `+`
47
+ - `` `https://analytics.example.com/people/${authUserId}` ``
48
+ - `fetch("/api/teams/" + slug + "/members")`
49
+ - `` `?q=${searchTerm}` ``
50
+
51
+ Suppress when:
52
+ - the substitution is wrapped in `encodeURIComponent(...)`,
53
+ - the URL is built with `url.searchParams.set(name, value)`;
54
+ the URLSearchParams encoder handles reserved chars (including
55
+ `/`, `=`, `&`) in the value,
56
+ - the value is encoded per-segment first and only then assembled
57
+ into a pathname (e.g. `url.pathname = "/users/" +
58
+ encodeURIComponent(userId)`),
59
+ - the substituted value is a literal / module-level constant /
60
+ enum member (compile-time-known safe),
61
+ - the substituted value is already known to be opaque-encoded
62
+ upstream (e.g. JWT, base64url); note the encoding source if
63
+ the diff makes that visible.
64
+
65
+ DO NOT suppress on `new URL(...)` alone. The URL constructor
66
+ parses; it does NOT encode reserved characters inside a
67
+ pre-assembled path. Likewise `url.pathname = "/users/" + userId`
68
+ does NOT encode `/` (or other reserved chars) within the path
69
+ segment: if the substituted `userId` contains a `/`, the route
70
+ still splits exactly as the lens is meant to catch. The
71
+ URL-class suppression only applies when the encoding happens at
72
+ the per-segment / per-param boundary, not at pathname assignment
73
+ time.
74
+
75
+ Severity: `minor` for display URLs that just break visually,
76
+ `major` when the unencoded value reaches the network layer and
77
+ could redirect a request to a different host or path (the `..`
78
+ and `@` escape cases).
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: use-state-default-not-functional
3
+ description: useState initialized with an expensive computation that re-runs every render
4
+ triggers:
5
+ files:
6
+ - "*.tsx"
7
+ - "*.jsx"
8
+ - "**/*.tsx"
9
+ - "**/*.jsx"
10
+ hunk_regex: "useState\\s*\\("
11
+ confidence_floor: 0.7
12
+ ---
13
+
14
+ `useState(expensiveCompute())` invokes `expensiveCompute()` on
15
+ EVERY render: React only uses the result on the first render but
16
+ the call still happens. For pure functions this is wasted CPU; for
17
+ calls with side effects (analytics fire, localStorage read,
18
+ expensive parse) it's a real bug.
19
+
20
+ Flag when `useState(expr)` initializer is:
21
+ - a function call that's NOT a cheap literal / variable read
22
+ - a `JSON.parse` / `localStorage.getItem` / `sessionStorage.getItem`
23
+ / a sync filesystem call / a heavy array build
24
+
25
+ Suppress when:
26
+ - the initializer is the lazy form `useState(() => expensiveCompute())`
27
+ (React only calls the function on mount)
28
+ - the initializer is a literal, a stable variable, or a cheap
29
+ primitive lookup
30
+ - the function is memoized externally and cheap on subsequent calls
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "openqodex",
3
+ "version": "0.1.0",
4
+ "description": "Open source code review that runs inside your coding agent, before you push.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "homepage": "https://github.com/openqodex/openqodex#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/openqodex/openqodex.git",
11
+ "directory": "packages/cli"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/openqodex/openqodex/issues"
15
+ },
16
+ "keywords": [
17
+ "code-review",
18
+ "static-analysis",
19
+ "claude-code",
20
+ "cursor",
21
+ "codex",
22
+ "cline",
23
+ "pre-commit"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "bin": {
29
+ "openqodex": "./dist/bin.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "lenses",
34
+ "docs",
35
+ "skills",
36
+ "templates",
37
+ "demo",
38
+ "toolchain.json",
39
+ "README.md",
40
+ "LICENSE",
41
+ "NOTICE"
42
+ ],
43
+ "dependencies": {},
44
+ "devDependencies": {
45
+ "@clack/prompts": "^1.8.1",
46
+ "commander": "^14.0.3",
47
+ "picocolors": "^1.1.1",
48
+ "yaml": "^2.9.1",
49
+ "zod": "^4.6.5",
50
+ "@openqodex/core": "0.1.0",
51
+ "@openqodex/scanners": "0.1.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit"
56
+ }
57
+ }
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: openqodex
3
+ description: Review the current code change before it is pushed. Runs the security and lint scanners that fit the changed files, then guides you through verifying their findings and reviewing the change yourself, and writes a report. Use before every git push, when asked to review changes, and when a push was blocked or warned by OpenQodex.
4
+ ---
5
+
6
+ # OpenQodex: review the change before it is pushed
7
+
8
+ OpenQodex runs deterministic scanners (gitleaks, semgrep, bandit, hadolint, shellcheck, actionlint, osv-scanner and others) on the files that changed, keeps only what they report on changed lines, and hands you a review brief. You review the change with your own tools and model, write your findings to a file in a fixed shape, and OpenQodex checks that file without a model and writes the report. No key and no account are needed. OpenQodex sends no code anywhere. One scanner goes online: when the change touches a dependency file, osv-scanner asks osv.dev about the names and versions of the dependencies; `--offline` on the review command skips that lookup. A custom scanner the developer approved does whatever its own command does.
9
+
10
+ ## When to run
11
+
12
+ - Before any `git push`.
13
+ - When the developer asks you to review their changes.
14
+ - When a push was blocked or warned by the OpenQodex hook.
15
+ - After fixing findings, to check the change again.
16
+
17
+ ## Procedure
18
+
19
+ 1. From the repository, run:
20
+
21
+ ```
22
+ npx -y openqodex@0.1.0 review --agent
23
+ ```
24
+
25
+ It works out the change (the commits not yet pushed plus everything uncommitted, untracked files included), runs the scanners and prints the brief. Read the whole brief before doing anything else.
26
+
27
+ 2. Verify each scanner candidate against the code. Every candidate has an id (`c1`, `c2`, ...) and a token like `[semgrep:python.lang.security.audit.formatted-sql-query]`. Open the file at the line and decide:
28
+ - real: raise it as a finding with `source` set to the token and `candidate` set to the id;
29
+ - not real (a test fixture, dead code, a pattern the code already guards): put it under `dropped` with a one-line reason.
30
+
31
+ Several candidates often describe one problem (two scanners, or two rules of one scanner, on the same line). Raise one of them and drop the others with the reason `duplicate of c<id>`.
32
+
33
+ Every candidate must end up in one of the two. A candidate you leave out is reported as "Not reviewed by the agent" and counts toward the verdict at its scanner severity.
34
+
35
+ 3. Weigh each pattern listed under "Patterns to weigh". Each one describes a kind of bug that changes like this one often carry. Check the changed lines against it. When a pattern leads you to a finding, set `source` to `lens:<name>`.
36
+
37
+ 4. Review the change yourself. Use your own tools to read the callers and the tests of every function the change touches. Look for wrong behaviour, missing checks, broken edge cases and changed behaviour with no test. Findings from your own reading have `source: null`.
38
+
39
+ 5. Write the findings to the exact path the brief names (it ends in `agent-findings.json`), in the shape below.
40
+
41
+ 6. Run:
42
+
43
+ ```
44
+ npx -y openqodex@0.1.0 review --finalize
45
+ ```
46
+
47
+ If it exits with code 2 and names a wrong field or a citation that does not match, fix what it names in your findings file and run finalize again. If it says the change moved or the config changed, run step 1 again and review from the new brief: the review must describe the change and the settings as they are now. Never change the developer's code or config to make finalize pass.
48
+
49
+ 7. Tell the developer the verdict, the counts by severity, the most serious findings in one line each, and the path of `report.md`. Do not paste the whole report.
50
+
51
+ ## The finding shape
52
+
53
+ ```json
54
+ {
55
+ "version": 1,
56
+ "change_id": "3f9a1c0b2d4e",
57
+ "summary": "Adds a search endpoint and a deploy script.",
58
+ "findings": [
59
+ {
60
+ "severity": "critical",
61
+ "category": "security",
62
+ "confidence": 0.9,
63
+ "file_path": "app/search.py",
64
+ "line_number": 14,
65
+ "line_end": 14,
66
+ "title": "SQL injection in item search",
67
+ "description": "The query is built with an f-string from request.args, so a caller controls the SQL. Pass the value as a query parameter.",
68
+ "suggested_change": "cur.execute(\"SELECT * FROM items WHERE name = ?\", (q,))",
69
+ "source": "semgrep:python.lang.security.audit.formatted-sql-query",
70
+ "candidate": "c2"
71
+ }
72
+ ],
73
+ "dropped": [
74
+ { "candidate": "c5", "reason": "test fixture, not a real key" }
75
+ ]
76
+ }
77
+ ```
78
+
79
+ - `change_id`: copy it from the brief.
80
+ - `summary`: what the change does, in one or two sentences. Not the findings.
81
+ - `file_path`: relative to the repository root. `line_number` and `line_end` point at the code line that holds the problem, never at a comment or a blank line, and at an import only when the import itself is the problem. `line_end` is optional and defaults to `line_number`.
82
+ - `title`: a short noun phrase naming the problem. No line numbers, no quoted code.
83
+ - `description`: one to three sentences: what is wrong, why it matters, the fix.
84
+ - `suggested_change`: the replacement text for the cited lines when the fix fits in a few lines, matching the indentation. Otherwise `null`, and explain the fix in `description`.
85
+ - `source`: `null` for your own finding, the candidate's token when raising a candidate, or `lens:<name>` when a listed pattern led to it.
86
+ - `candidate`: the candidate id when raising one, else leave it out. The id and the token must belong to the same candidate.
87
+ - `confidence`: from 0 to 1, how sure you are that the problem is real, based on what you read.
88
+
89
+ Severity says how much harm the problem does, not how sure you are:
90
+
91
+ - `critical`: data loss, a security breach, a crash on a common path, broken authentication.
92
+ - `major`: wrong behaviour under realistic conditions, a performance regression, a broken edge case someone would be paged for.
93
+ - `minor`: a real bug that is unlikely to show in practice.
94
+ - `nitpick`: style, naming or a convention preference.
95
+ - `info`: worth knowing, no action needed.
96
+
97
+ Category says what kind of problem it is:
98
+
99
+ - `bug`: the code does the wrong thing.
100
+ - `security`: the code can be abused, or leaks something it should not.
101
+ - `performance`: the code is slower or uses more resources than it needs to.
102
+ - `maintainability`: the code works but is hard to change safely (missing test, duplicated logic, unclear structure).
103
+ - `style`: formatting, naming and conventions.
104
+
105
+ ## Rules
106
+
107
+ - Raise only what you verified in the code. A guess with nothing in the code to point at is not a finding.
108
+ - A finding with confidence under 0.7 is not raised. Finalize drops it and lists it as low confidence.
109
+ - Every scanner candidate is either raised or listed under `dropped` with a reason.
110
+ - Never edit code during the review. Review first, report, then fix only what the developer asks you to fix.
111
+ - Never run `openqodex trust` without asking the developer first. It approves a custom scanner, which is a command that runs on their machine.
112
+ - Never set `OPENQODEX_SKIP`. It is the developer's switch, not yours.
113
+ - When the verdict is `blocked`, do not push. Show the developer the findings; push only if they say so after seeing them.
114
+ - An empty findings list is a valid review. Do not pad it.
115
+
116
+ ## Reading the report
117
+
118
+ - The report is in `.openqodex/reviews/<time>-<id>/` in the repository: `report.md` to read, `report.json` and `report.sarif` for tools. `.openqodex/latest.json` points at the newest one. The folder ignores itself in git, so it never shows in `git status`.
119
+ - The verdict is `passed` (with or without warnings) or `blocked`. It is `blocked` only when the repository's `.openqodex.yaml` sets `block_on_severity` and a finding is at or above it. With no config, OpenQodex warns and never blocks.
120
+ - "Outside the changed lines" lists findings on lines the developer did not change. They are shown but never count toward the verdict.
121
+ - The coverage list says, for each scanner, whether it ran. A scanner that did not run has a one-line reason:
122
+ - `no matching files`: nothing in the change is the kind of file it reads.
123
+ - `installing`: it is being downloaded for the first time; it is included from the next run. Say so to the developer rather than waiting.
124
+ - `not installed`: it could not be installed here; the reason says why.
125
+ - `needs Ruby 2.7+` or `needs Go`: brakeman and rubocop need Ruby, golangci-lint needs Go. OpenQodex does not install language runtimes. If the developer wants those scanners, they install Ruby or Go the usual way for their system (for example `brew install ruby go` on a Mac) and run the review again.
126
+ - `untrusted`: a custom scanner from `.openqodex.yaml` that the developer has not approved. Tell the developer; approving it is their decision (`npx -y openqodex@0.1.0 trust`).
127
+ - `failed`: the scanner ran and broke; the reason has its error. A scanner problem never changes the exit code.
128
+
129
+ ## Inside a sandbox
130
+
131
+ Some agents run commands in a sandbox that cannot reach the network or write outside the project. There the first run cannot download the scanners, and each scanner reports why it was not included. The review still runs with whatever is available. Tell the developer to run this once in their own terminal, outside the agent:
132
+
133
+ ```
134
+ npx -y openqodex@0.1.0 doctor --install
135
+ ```
136
+
137
+ It downloads every scanner that fits the machine into `~/.openqodex/tools/`. After that, reviews inside the sandbox include them.
138
+
139
+ ## More
140
+
141
+ `npx -y openqodex@0.1.0 guide` prints this guide. `npx -y openqodex@0.1.0 guide <topic>` prints a page of the docs, offline: `quickstart`, `config`, `scanners`, `custom-scanners`, `security`, `agents`, `cli`.
@@ -0,0 +1,67 @@
1
+ # Templates that `openqodex init` writes
2
+
3
+ Each file here is copied or merged by `openqodex init`. Two placeholders are filled at install time and no others exist:
4
+
5
+ - `{{VERSION}}`: the version of the running `openqodex` package.
6
+ - `{{LAUNCHER}}`: the absolute path of the launcher, `~/.openqodex/bin/openqodex` expanded.
7
+
8
+ The skill itself is not a template: `init` copies `skills/openqodex/SKILL.md` from the package unchanged.
9
+
10
+ User scope is the default. Project scope (`--project`) writes into the repository for a team to commit. A repository file written in user scope is added to `.git/info/exclude` so `git status` does not change.
11
+
12
+ Every path below was read from the source named beside it on 2026-10-01. Anything marked "assumption, untested" was not confirmed and must not be written by `init` as if it were.
13
+
14
+ ## Claude Code
15
+
16
+ | What | Template | User scope | Project scope |
17
+ |---|---|---|---|
18
+ | Skill | `skills/openqodex/SKILL.md` | `~/.claude/skills/openqodex/SKILL.md` | `.claude/skills/openqodex/SKILL.md` |
19
+ | Push gate hook | `claude-code/settings-hook.json`, merged | `~/.claude/settings.json` | `.claude/settings.json` |
20
+
21
+ - Settings paths: https://code.claude.com/docs/en/hooks, section "Hook locations".
22
+ - Skill paths: the `skills` CLI agent table (github.com/vercel-labs/skills, README, "Supported agents"), and the same hooks page, which names `~/.claude/skills/` and `.claude/skills/`.
23
+ - The hook: `matcher: "Bash"` with `if: "Bash(git push*)"` on the handler. The `if` field uses permission-rule syntax and is checked against each subcommand (same hooks page, "Bash if matching"). The page also says a pattern longer than the command name runs the hook anyway when the command holds `$()`, backticks or `$VAR`, so `hook check` must itself confirm the command is a push.
24
+ - Merge rule: append the one entry under `hooks.PreToolUse`, keep every other key, do nothing when an entry with the same command already exists.
25
+
26
+ ## Codex CLI
27
+
28
+ | What | Template | User scope | Project scope |
29
+ |---|---|---|---|
30
+ | Skill | `skills/openqodex/SKILL.md` | see the note below | `.agents/skills/openqodex/SKILL.md` |
31
+ | Instructions | `codex/AGENTS-section.md`, between its markers | not written | `AGENTS.md` (replace the text between the markers, or append) |
32
+ | Push gate hook | `codex/hooks.json`, merged | `~/.codex/hooks.json` | `.codex/hooks.json` |
33
+
34
+ - Hook file paths, schema and output: https://learn.chatgpt.com/docs/hooks (where https://developers.openai.com/codex/hooks redirects). `codex features list` on Codex CLI 0.160.0 shows `hooks` as stable and on.
35
+ - Codex hooks have no `if` field: the matcher is a regular expression on the tool name only. The hook therefore runs before every shell command, and `hook check` must abstain at once, printing nothing, when the command is not a `git push`.
36
+ - Codex runs a new user or project hook only after the developer reviews and trusts it with `/hooks` inside Codex; project hooks load only in a trusted project. `init` must print that step.
37
+ - Codex's PreToolUse output supports `permissionDecision` deny, `additionalContext` and `systemMessage`; `ask` is parsed but not implemented. Exit code 2 with the reason on stderr also denies.
38
+ - Skill path conflict: the `skills` CLI table puts the Codex user skill in `~/.codex/skills/`; the Codex docs (https://learn.chatgpt.com/docs/build-skills) list `$HOME/.agents/skills` and repository `.agents/skills`, and do not list `~/.codex/skills/`. Write `~/.agents/skills/openqodex/SKILL.md`, which the Codex docs name. That Codex 0.160.0 still reads `~/.codex/skills/`: assumption, untested.
39
+
40
+ ## Cursor
41
+
42
+ | What | Template | User scope | Project scope |
43
+ |---|---|---|---|
44
+ | Skill | `skills/openqodex/SKILL.md` | `~/.cursor/skills/openqodex/SKILL.md` | `.agents/skills/openqodex/SKILL.md` |
45
+ | Rule | `cursor/openqodex.mdc` | `.cursor/rules/openqodex.mdc` in the repository, excluded from git | `.cursor/rules/openqodex.mdc` |
46
+
47
+ - Rule location and frontmatter: https://cursor.com/docs/context/rules. Project rules are `.mdc` files in `.cursor/rules`; the fields are `description`, `globs` and `alwaysApply`; `alwaysApply: true` makes the rule apply to every chat. User rules live in Cursor's settings, not on disk, so there is no user-level rule file.
48
+ - Skill paths: the `skills` CLI table, and https://cursor.com/docs/context/skills, which lists `.agents/skills/`, `.cursor/skills/`, `~/.agents/skills/` and `~/.cursor/skills/` (and the Claude and Codex folders for compatibility).
49
+ - Cursor hooks (`.cursor/hooks.json`) were not checked: no Cursor hook is written. Assumption, untested, that a rule alone is enough for Cursor to review before pushing.
50
+
51
+ ## Cline
52
+
53
+ | What | Template | User scope | Project scope |
54
+ |---|---|---|---|
55
+ | Skill | `skills/openqodex/SKILL.md` | `~/.cline/skills/openqodex/SKILL.md` | `.cline/skills/openqodex/SKILL.md` |
56
+ | Rule | `cline/openqodex.md` | `~/Documents/Cline/Rules/openqodex.md` | `.clinerules/openqodex.md` |
57
+
58
+ - Rule paths: https://docs.cline.bot/features/cline-rules. Cline reads every file in `.clinerules/` (or `.cline/rules/`) at the project root; global rules are in `~/Documents/Cline/Rules` on macOS and Linux (`Documents\Cline\Rules` on Windows), with `~/.cline/rules` and `~/Cline/Rules` also searched. A rule with no frontmatter always applies. The plan's default (rule in the repository, excluded from git) also works; the global rule folder avoids touching the repository.
59
+ - Skill path conflict: the `skills` CLI table puts Cline skills in `.agents/skills/` and `~/.agents/skills/`; Cline's docs (https://docs.cline.bot/features/skills) list `.cline/skills/`, `.clinerules/skills/`, `.claude/skills/` and `~/.cline/skills/`, and not `.agents/skills/`. Write the path Cline's docs name. That Cline reads `.agents/skills/`: assumption, untested.
60
+
61
+ ## Not written on Day 0
62
+
63
+ No MCP server configuration is written for any agent.
64
+
65
+ ## Placeholders
66
+
67
+ `{{VERSION}}` is the running package version. `{{LAUNCHER}}` is the absolute launcher path, and `init` must substitute it already quoted for a POSIX shell (single quotes, with any single quote inside escaped), because a home folder can contain a space and the hook command is run through a shell.
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "if": "Bash(git push*)",
10
+ "command": "{{LAUNCHER}} hook check"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ # Review before push with OpenQodex
2
+
3
+ Before any `git push`, and whenever you are asked to review the changes, review the change with OpenQodex:
4
+
5
+ 1. Run `npx -y openqodex@{{VERSION}} review --agent` from the repository and read the brief it prints.
6
+ 2. Follow the openqodex skill for the rest of the review: verify each scanner candidate, write your findings to the path the brief names, then run `npx -y openqodex@{{VERSION}} review --finalize`. If the skill is not loaded, run `npx -y openqodex@{{VERSION}} guide` to print it.
7
+ 3. Show the developer the verdict and the report path. When the verdict is `blocked`, do not push unless the developer says so after seeing the findings.
8
+
9
+ Never edit code during the review, never run `openqodex trust` without asking the developer, and never set `OPENQODEX_SKIP`.
@@ -0,0 +1,8 @@
1
+ <!-- openqodex:start -->
2
+ ## Review before push (OpenQodex)
3
+ OpenQodex is open source code review that runs scanners on the change and guides your own review before it is pushed.
4
+ Before any `git push`, and when asked to review the changes, run `npx -y openqodex@{{VERSION}} review --agent` and follow the openqodex skill (`npx -y openqodex@{{VERSION}} guide` prints it).
5
+ Write findings to the path the brief names, run `npx -y openqodex@{{VERSION}} review --finalize`, then show the developer the verdict and the report in `.openqodex/reviews/`.
6
+ If scanners cannot install inside the sandbox, ask the developer to run `npx openqodex doctor --install` once in their own terminal.
7
+ Do not push when the verdict is `blocked` unless the developer says so after seeing the findings.
8
+ <!-- openqodex:end -->
@@ -0,0 +1,16 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "^Bash$",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "{{LAUNCHER}} hook check",
10
+ "statusMessage": "OpenQodex: checking for a review before push"
11
+ }
12
+ ]
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ description: Review the change with OpenQodex before any git push
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+
7
+ # Review before push with OpenQodex
8
+
9
+ Before any `git push`, and whenever you are asked to review the changes, review the change with OpenQodex:
10
+
11
+ 1. Run `npx -y openqodex@{{VERSION}} review --agent` from the repository and read the brief it prints.
12
+ 2. Follow the openqodex skill for the rest of the review: verify each scanner candidate, write your findings to the path the brief names, then run `npx -y openqodex@{{VERSION}} review --finalize`. If the skill is not loaded, run `npx -y openqodex@{{VERSION}} guide` to print it.
13
+ 3. Show the developer the verdict and the report path. When the verdict is `blocked`, do not push unless the developer says so after seeing the findings.
14
+
15
+ Never edit code during the review, never run `openqodex trust` without asking the developer, and never set `OPENQODEX_SKIP`.