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,73 @@
1
+ # GitHub Action
2
+
3
+ The OpenQodex Action runs `openqodex scan` on a pull request's change. It uploads the findings to GitHub code scanning as SARIF. No model is involved: the Action runs the scanners only.
4
+
5
+ ## Example workflow
6
+
7
+ Save this as `.github/workflows/openqodex.yml`:
8
+
9
+ ```yaml
10
+ name: OpenQodex
11
+
12
+ on:
13
+ pull_request:
14
+
15
+ permissions:
16
+ contents: read
17
+ security-events: write
18
+ actions: read
19
+
20
+ jobs:
21
+ scan:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v7
25
+ with:
26
+ fetch-depth: 0
27
+ - uses: openqodex/openqodex@v0
28
+ ```
29
+
30
+ - `fetch-depth: 0` fetches the full history. The scan needs the pull request's base commit, which a shallow checkout does not have.
31
+ - `security-events: write` lets the Action upload SARIF to code scanning.
32
+ - `actions: read` is read by the SARIF upload in a private repository.
33
+ - `contents: read` lets the job check out the code.
34
+
35
+ ## Inputs
36
+
37
+ - `version`: the `openqodex` version to run. The default is the version the Action was released with.
38
+ - `upload-sarif`: `true` or `false`. The default is `true`. Set it to `false` to skip the code scanning upload.
39
+
40
+ ## What it does
41
+
42
+ 1. Sets up Node 22.
43
+ 2. Restores `~/.openqodex/tools` from the Actions cache, keyed on the runner and the `openqodex` version.
44
+ 3. Runs `npx -y openqodex@<version> doctor --install`, which installs every scanner and waits.
45
+ 4. Runs `npx -y openqodex@<version> scan --base <pull request base commit> --format sarif`. The SARIF goes to a new folder under the runner's temporary folder, never into the checkout.
46
+ 5. Uploads that SARIF to code scanning, when `upload-sarif` is `true` and the scan wrote a report.
47
+ 6. Fails the job when the scan exited 1 or 2.
48
+
49
+ The scan exits 1 only when `.openqodex.yaml` sets `review.block_on_severity` and a finding on a changed line meets it. Without that key, the job never fails on findings. A scan that fails for its own reasons exits 2, and the job fails too.
50
+
51
+ ## Config
52
+
53
+ The Action reads `.openqodex.yaml` from the repository, like every other command. Custom scanners need an approval stored on the machine that runs them. The runner has none, so the Action lists custom scanners as `untrusted` and skips them.
54
+
55
+ ## Pre-commit
56
+
57
+ The repository also ships a pre-commit hook for the pre-push stage. Add this to `.pre-commit-config.yaml`:
58
+
59
+ ```yaml
60
+ repos:
61
+ - repo: https://github.com/openqodex/openqodex
62
+ rev: v0.1.0
63
+ hooks:
64
+ - id: openqodex-scan
65
+ ```
66
+
67
+ Then install the pre-push hook:
68
+
69
+ ```
70
+ pre-commit install --hook-type pre-push
71
+ ```
72
+
73
+ The hook runs `npx -y openqodex@<version> scan` on the commits not yet pushed plus the working tree. It stops the push only when the scan exits 1. A scan that fails for its own reasons never stops the push. The hook needs Node 22, npx and `sh` on your machine.
package/docs/index.md ADDED
@@ -0,0 +1,29 @@
1
+ # OpenQodex docs
2
+
3
+ OpenQodex is open source code review that runs inside your coding agent, before you push.
4
+
5
+ These pages ship inside the npm package. `npx openqodex guide <topic>` prints one in your terminal, offline. The topic is the file name without `.md`.
6
+
7
+ ## Words used in these pages
8
+
9
+ - Change: the commits not yet pushed plus everything uncommitted, untracked files included.
10
+ - Changed lines: the lines the change adds or edits. Context lines around them do not count.
11
+ - Scanner: a program that checks code without a model, such as gitleaks or semgrep.
12
+ - Finding: one problem at one place in the code, with a severity.
13
+ - Candidate: a scanner finding on a changed line, waiting for the agent to verify it.
14
+ - Brief: the text `openqodex review --agent` prints for the agent to review from.
15
+ - Report: the result of a scan or a review, written as `report.md`, `report.json` and `report.sarif`.
16
+ - Verdict: `passed` or `blocked`.
17
+
18
+ ## Pages
19
+
20
+ - `quickstart`: install OpenQodex and run the first review.
21
+ - `cli`: every command, flag and exit code.
22
+ - `config`: every key of `.openqodex.yaml`.
23
+ - `scanners`: the thirteen built-in scanners.
24
+ - `custom-scanners`: add any scanner by its GitHub link.
25
+ - `agents`: what `init` writes for each coding agent.
26
+ - `github-action`: run the scan on pull requests.
27
+ - `security`: what runs, what is sent where, and where files go.
28
+ - `telemetry`: there is none.
29
+ - `faq`: short answers.
package/docs/llms.txt ADDED
@@ -0,0 +1,13 @@
1
+ # OpenQodex docs
2
+
3
+ - [Agents](agents.md)
4
+ - [Commands](cli.md)
5
+ - [Configuration](config.md)
6
+ - [Custom scanners](custom-scanners.md)
7
+ - [FAQ](faq.md)
8
+ - [GitHub Action](github-action.md)
9
+ - [OpenQodex docs](index.md)
10
+ - [Quickstart](quickstart.md)
11
+ - [Scanners](scanners.md)
12
+ - [Security](security.md)
13
+ - [Telemetry](telemetry.md)
@@ -0,0 +1,82 @@
1
+ # Quickstart
2
+
3
+ ## Paste this prompt into your agent
4
+
5
+ ```
6
+ Install the OpenQodex skill with `npx skills add openqodex/openqodex`.
7
+ Then review my current change with openqodex and tell me the verdict and the findings.
8
+ ```
9
+
10
+ The agent installs the skill, runs the review and tells you the result. The steps below do the same by hand.
11
+
12
+ ## Before you start
13
+
14
+ - Node 22 or newer, and git.
15
+ - macOS or Linux. On Windows, use WSL.
16
+ - A git repository with a change in it.
17
+
18
+ ## 1. Install into your agent
19
+
20
+ Run this in your own terminal, not inside the agent:
21
+
22
+ ```
23
+ npx openqodex init
24
+ ```
25
+
26
+ `init` finds Claude Code, Cursor, Codex CLI and Cline on your machine. It prints each file it will write, then asks once. `--yes` skips the question. `agents` lists every file for each agent.
27
+
28
+ `init` also starts the scanner downloads that your repo needs, in the background. Running it outside the agent matters: some agents run commands in a sandbox that cannot download.
29
+
30
+ Codex only: open Codex, run `/hooks` and trust the OpenQodex hook. Codex runs a new hook only after you trust it.
31
+
32
+ ## 2. Ask for a review
33
+
34
+ Say to your agent:
35
+
36
+ ```
37
+ review my change with openqodex
38
+ ```
39
+
40
+ The agent runs `openqodex review --agent`. That command works out the change, runs the scanners and prints a brief. The agent verifies each scanner finding, reviews the change itself, and writes its findings to a file. Then it runs `openqodex review --finalize`, which checks those findings without a model and writes the report.
41
+
42
+ ## 3. Read the report
43
+
44
+ The agent tells you the verdict and the most serious findings. The full report is in `.openqodex/reviews/<time>-<id>/report.md` in your repo. `.openqodex/` ignores itself in git, so `git status` does not change.
45
+
46
+ The verdict is `passed` unless `.openqodex.yaml` sets `review.block_on_severity` and a finding meets it. With no config, OpenQodex warns and never blocks.
47
+
48
+ ## Try it on the demo repo
49
+
50
+ ```
51
+ npx openqodex demo /tmp/openqodex-demo
52
+ ```
53
+
54
+ `demo` builds a small repo with planted bugs: a secret, a SQL injection, a bad Dockerfile, a vulnerable lockfile, a shell bug and a workflow injection. It scans the change and prints the report. Then open the folder in your agent and ask for a review.
55
+
56
+ ## Without an agent
57
+
58
+ `openqodex scan` runs the scanners on the change and prints the report:
59
+
60
+ ```
61
+ npx openqodex scan
62
+ ```
63
+
64
+ It is the same check the git hook, the pre-commit hook and the GitHub Action run.
65
+
66
+ ## First run
67
+
68
+ Scanners download on first use into `~/.openqodex/tools/`. Only the scanners your change needs download. A scanner still installing after 45 seconds keeps going in the background. The report lists it as installing. It joins the next run.
69
+
70
+ One measured first run: an Apple Silicon Mac, an empty tool folder, a line of 2 MB per second. The first `demo` printed its report in under a minute. That report held the scanners that had finished installing and listed the rest as installing. The next `scan` included all eight scanners the demo needs. They take about 700 MB of disk.
71
+
72
+ To download every scanner now:
73
+
74
+ ```
75
+ npx openqodex doctor --install
76
+ ```
77
+
78
+ ## Next
79
+
80
+ - `config`: block pushes at a severity, exclude paths, switch scanners off.
81
+ - `custom-scanners`: add any scanner by its GitHub link.
82
+ - `security`: what runs and what is sent where.
@@ -0,0 +1,148 @@
1
+ # Scanners
2
+
3
+ OpenQodex has thirteen built-in scanners. Each one runs only when the change holds a file it reads. Every downloaded scanner is pinned to one version. OpenQodex never uses a copy of a built-in scanner from your `PATH`.
4
+
5
+ Pinned versions do not make findings identical on every machine. semgrep fetches its registry rule packs at run time, and OpenQodex does not pin their version.
6
+
7
+ Every scanner reads the whole changed file. OpenQodex keeps only the findings on changed lines.
8
+
9
+ ## Where scanners come from
10
+
11
+ Scanners download on first use into `~/.openqodex/tools/<scanner>/<version>/`. `OPENQODEX_HOME` moves that folder.
12
+
13
+ - GitHub release files are checked against the sha256 pinned in the package before they are unpacked.
14
+ - semgrep and bandit install from PyPI through uv, into one Python 3.11 that OpenQodex manages. uv comes from your `PATH` when present. Otherwise OpenQodex downloads a pinned uv.
15
+ - oxlint installs from npm, with the npm that ships beside your Node. Package install scripts are switched off.
16
+ - brakeman and rubocop install from RubyGems with your Ruby's `gem` command.
17
+
18
+ A scanner install that takes longer than 45 seconds keeps going in the background. The report lists that scanner as installing. The scanner joins the next run. `openqodex doctor --install` installs every scanner and waits.
19
+
20
+ Installed scanners take more disk than their downloads. The eight scanners the demo needs take about 700 MB of disk on an Apple Silicon Mac. semgrep with its Python takes about 440 MB of that.
21
+
22
+ OpenQodex does not install Ruby or Go. Without them, the report lists the scanners that need them as not installed, with the reason.
23
+
24
+ ## Status in the report
25
+
26
+ The report lists every selected scanner with one status. A scanner left out with `--only` or `--skip` is not listed.
27
+
28
+ - `ran`: it ran.
29
+ - `no_matching_files`: the change holds no file it reads.
30
+ - `installing`: it is downloading for the first time. It joins the next run.
31
+ - `not_installed`: it could not be installed here. The reason says why.
32
+ - `failed`: it ran and broke. The reason holds its error.
33
+ - `disabled`: `scanners.disable` names it, or `--offline` skipped it.
34
+ - `untrusted`: a custom scanner you have not approved.
35
+
36
+ A scanner problem never changes the exit code.
37
+
38
+ ## semgrep
39
+
40
+ - Version: 1.94.0.
41
+ - Runs when: any file changed.
42
+ - Needs: Python 3.11, which OpenQodex downloads through uv. About 86 MB with uv and bandit, measured on Apple Silicon.
43
+ - Rules: the registry packs `p/default`, `p/security-audit` and `p/secrets`.
44
+ - Sends: semgrep fetches those rule packs from the Semgrep registry on each run. It runs with its metrics switched off. The rules are never bundled in the OpenQodex package.
45
+ - `--offline` skips it. The report lists it as disabled.
46
+
47
+ ## gitleaks
48
+
49
+ - Version: 8.21.2.
50
+ - Runs when: any file changed.
51
+ - Needs: nothing. 2.9 MB on Apple Silicon, 3.0 MB on Linux x64.
52
+ - Writes: links the changed files into a temporary folder outside the repo and scans that folder. It reads the repo's `.gitleaks.toml` or `gitleaks.toml` when present.
53
+ - gitleaks writes its raw report to a temporary file outside the repo. That file holds the matched secrets. OpenQodex deletes it when the run ends.
54
+ - Sends: nothing.
55
+ - Secrets it finds are redacted from the brief, every report file and the terminal. No file OpenQodex keeps holds the secret.
56
+
57
+ ## bandit
58
+
59
+ - Version: 1.9.4.
60
+ - Runs when: a `.py` or `.pyi` file changed.
61
+ - Needs: the same Python as semgrep.
62
+ - Sends: nothing.
63
+
64
+ ## ruff
65
+
66
+ - Version: 0.8.4.
67
+ - Runs when: a `.py` or `.pyi` file changed.
68
+ - Needs: nothing. 9.9 MB on Apple Silicon, 11.2 MB on Linux x64.
69
+ - Reads the repo's own ruff settings. It runs with fixes and its cache switched off, so it changes no file.
70
+ - Sends: nothing.
71
+
72
+ ## oxlint
73
+
74
+ - Version: 1.71.0.
75
+ - Runs when: a `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts` or `.cts` file changed.
76
+ - Needs: npm, which ships with Node. About 7.3 MB on Apple Silicon, 8.2 MB on Linux x64.
77
+ - Uses OpenQodex's own settings. A config file in the repo is not loaded.
78
+ - Sends: nothing.
79
+
80
+ ## osv-scanner
81
+
82
+ - Version: 1.9.2.
83
+ - Runs when: one of these lockfiles changed: `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `Cargo.lock`, `go.mod`, `go.sum`, `requirements.txt`, `Pipfile.lock`, `poetry.lock`, `Gemfile.lock`, `composer.lock`, `pom.xml`, `gradle.lockfile`, `pubspec.lock`, `mix.lock`, `conan.lock`.
84
+ - Needs: network access to osv.dev. 31.8 MB on Apple Silicon, 32.1 MB on Linux x64.
85
+ - Sends: the names and versions of the dependencies in those lockfiles, to osv.dev. It never sends code.
86
+ - `--offline` skips it. The report lists it as disabled.
87
+
88
+ ## actionlint
89
+
90
+ - Version: 1.7.7.
91
+ - Runs when: a `.yml` or `.yaml` file under `.github/workflows/` changed.
92
+ - Needs: nothing. 2.0 MB on Apple Silicon, 2.1 MB on Linux x64.
93
+ - Sends: nothing.
94
+
95
+ ## hadolint
96
+
97
+ - Version: 2.15.1.
98
+ - Runs when: a Dockerfile changed: `Dockerfile`, `Dockerfile.<name>` or `<name>.dockerfile`.
99
+ - Needs: nothing. 102.6 MB on Apple Silicon, 55.7 MB on Linux x64.
100
+ - Sends: nothing.
101
+
102
+ ## shellcheck
103
+
104
+ - Version: 0.10.0.
105
+ - Runs when: a `.sh` or `.bash` file changed.
106
+ - Needs: `xz` to unpack the download. 7.2 MB on Apple Silicon, 2.4 MB on Linux x64.
107
+ - Sends: nothing.
108
+
109
+ ## golangci-lint
110
+
111
+ - Version: 2.12.2. Its name in `.openqodex.yaml` is `golangci`.
112
+ - Runs when: a `.go` file changed. It checks the packages that hold the changed files.
113
+ - Needs: Go on your `PATH`. 14.4 MB on Apple Silicon, 15.0 MB on Linux x64.
114
+ - Uses OpenQodex's own settings, with gosec switched on. A `.golangci.yml` in the repo is not loaded. It never rewrites `go.mod` or `go.sum`.
115
+ - golangci-lint 2.12.2 is built with Go 1.26. With a newer Go on your PATH it cannot check the code. The report then lists golangci as failed, with the reason.
116
+ - Runs with the Go module proxy off. The modules the repo needs must already be in your Go module cache. Nothing is downloaded.
117
+ - Sends: nothing.
118
+
119
+ ## brakeman
120
+
121
+ - Version: 6.2.1.
122
+ - Runs when: a Ruby or Rails file changed, and the repo has a `Gemfile` and an `app/` folder. The files are `.rb`, `.rake`, `.gemspec`, `.erb`, `.haml`, `.slim`, `Gemfile`, `Rakefile` and `config.ru`.
123
+ - Needs: Ruby 2.7 or newer. It installs from RubyGems.
124
+ - Uses OpenQodex's own settings. The repo's brakeman config is not loaded.
125
+ - Sends: nothing.
126
+ - Licence: the Brakeman Public Use License, which is not an open source licence. OpenQodex does not bundle brakeman. It downloads brakeman at run time onto your machine. Read the licence before you use it, or switch it off with `scanners.disable: [brakeman]`.
127
+
128
+ ## rubocop
129
+
130
+ - Version: 1.69.2, with rubocop-rails 2.28.0 and rubocop-performance 1.23.0.
131
+ - Runs when: a `.rb`, `.rake` or `.gemspec` file, a `Gemfile` or a `Rakefile` changed.
132
+ - Needs: Ruby 2.7 or newer. It installs from RubyGems.
133
+ - Uses OpenQodex's own settings. A `.rubocop.yml` in the repo is not loaded, because it can load Ruby code.
134
+ - Sends: nothing.
135
+
136
+ ## sqllint
137
+
138
+ - Version: part of OpenQodex.
139
+ - Runs when: a `.sql` file changed.
140
+ - Needs: nothing. It runs inside OpenQodex and downloads nothing.
141
+ - Checks Postgres migrations for common mistakes, such as a privileged function left callable by every role.
142
+ - Sends: nothing.
143
+
144
+ ## Choosing scanners
145
+
146
+ - `scanners.disable` in `.openqodex.yaml` switches built-in scanners off.
147
+ - `--only` and `--skip` on `scan` and `review` pick scanners for one run.
148
+ - `custom-scanners` explains how to add any other scanner.
@@ -0,0 +1,74 @@
1
+ # Security
2
+
3
+ This page says what OpenQodex runs, what it sends where, and what it writes. Report a vulnerability through GitHub's private vulnerability reporting on the openqodex/openqodex repository. Never open a public issue for one.
4
+
5
+ ## What runs
6
+
7
+ - The `openqodex` CLI, on your Node.
8
+ - The built-in scanners that fit the change, from `~/.openqodex/tools/`. Each is pinned to one version. A built-in scanner is never taken from your `PATH`.
9
+ - Custom scanners from `.openqodex.yaml` that you approved with `openqodex trust`.
10
+ - git, from your `PATH`.
11
+
12
+ OpenQodex starts every program with an argument list, never through a shell. Scanners get a small set of environment variables: `PATH`, `HOME`, `TMPDIR`, `LANG`, the `LC_` variables, the proxy variables, and what the scanner itself needs. Your other variables, such as API keys, are not passed on.
13
+
14
+ A repository can hold config files that make a scanner run code or rewrite files. OpenQodex does not load such files for oxlint, golangci-lint, brakeman and rubocop. It uses its own settings for them. ruff and gitleaks read the repository's own settings for rules only. ruff runs with fixes switched off.
15
+
16
+ ## The trust step
17
+
18
+ A custom scanner is an arbitrary command. It runs on your machine with your permissions. The config file that names it comes from whatever repository you cloned. So nothing installs or runs a custom scanner until you approve that exact entry:
19
+
20
+ ```
21
+ npx openqodex trust
22
+ ```
23
+
24
+ `trust` downloads the release asset to a quarantine folder before it asks. Nothing is installed or run before your yes. `scan` and `review` never download a custom scanner. `trust` prints the version, the asset, its sha256, the program and the run line, then asks yes or no. The approval covers that repository and that entry only. An edited entry needs a new approval. `scan` and `review` skip an unapproved entry and list it as `untrusted`.
25
+
26
+ The stored sha256 is checked against the project's checksum file when the project publishes one. Otherwise it is the hash of your first download. `custom-scanners` explains the difference.
27
+
28
+ Agents that follow the OpenQodex skill are told never to run `openqodex trust` without asking you.
29
+
30
+ ## What is sent where
31
+
32
+ OpenQodex and the built-in scanners send no code anywhere. The review runs on the model your agent already uses, which sees what the agent reads. A custom scanner you approved does whatever its own command does.
33
+
34
+ OpenQodex and the built-in scanners use the network for these things only:
35
+
36
+ - Scanner downloads on first use. GitHub release files are checked against sha256 sums pinned in the package. semgrep and bandit come from PyPI through uv, with a Python 3.11 that uv downloads. oxlint comes from npm. brakeman and rubocop come from RubyGems. These package installs are pinned by version.
37
+ - Semgrep rule packs. semgrep fetches `p/default`, `p/security-audit` and `p/secrets` from the Semgrep registry on each run. Its metrics are off. The rules are never bundled in the package.
38
+ - The dependency check. When the change holds a lockfile, osv-scanner sends the names and versions of the dependencies in it to osv.dev. It never sends code.
39
+ - Custom scanners. `openqodex trust` reads the release from the GitHub API and downloads the asset. After approval, a custom scanner does whatever its own command does.
40
+
41
+ golangci-lint runs with the Go module proxy off, so it downloads no modules.
42
+
43
+ `--offline` skips osv-scanner and semgrep, which the report lists as disabled. It also turns scanner downloads off.
44
+
45
+ OpenQodex sends no telemetry. See `telemetry`.
46
+
47
+ ## Secrets
48
+
49
+ When gitleaks finds a secret in the change, OpenQodex removes it from the brief, every report file and the terminal. It keeps the length and sha256 of each secret, to redact any text the agent quotes.
50
+
51
+ gitleaks writes its raw report to a temporary file outside the repository. That file holds the matched secrets. OpenQodex deletes it when the run ends. No file OpenQodex keeps holds the secret.
52
+
53
+ A secret is redacted only when a scanner matched it. When gitleaks did not run, the brief shows the change as it is.
54
+
55
+ ## Where files are written
56
+
57
+ In your home folder, under `~/.openqodex/` (`OPENQODEX_HOME` moves it):
58
+
59
+ - `tools/<scanner>/<version>/`: the scanners.
60
+ - `tools/uv-python/`: the Python 3.11 for semgrep and bandit.
61
+ - `cache/`: the download caches for uv and npm.
62
+ - `runtime/<version>/` and `bin/openqodex`: the copy of the package and the launcher that the hooks call, written by `init`.
63
+ - `install.json`: what `init` and `hook install` wrote, so an uninstall removes only that.
64
+ - `trust.json`: your approvals of custom scanners.
65
+
66
+ In the repository, under `.openqodex/` only:
67
+
68
+ - `.gitignore`, holding `*`, so the folder ignores itself and `git status` does not change.
69
+ - `reviews/<time>-<id>/`: one folder per run, holding the brief, the scan result, the agent's findings and the reports. OpenQodex keeps the newest 20.
70
+ - `latest.json`: points at the newest run.
71
+
72
+ The agent settings and skill files `init` writes are listed in `agents`.
73
+
74
+ The change itself is worked out without writing inside `.git`. OpenQodex uses a temporary copy of the index and a temporary object folder.
@@ -0,0 +1,9 @@
1
+ # Telemetry
2
+
3
+ OpenQodex sends no telemetry.
4
+
5
+ It collects no usage data, no crash reports and no identifiers. It has no switch to turn telemetry on, because there is none to turn on.
6
+
7
+ semgrep runs with its own metrics switched off.
8
+
9
+ The network traffic OpenQodex does cause is listed in `security`: scanner downloads, Semgrep rule packs, the osv.dev dependency lookup, and custom scanner downloads you approve.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: a11y-icon-only-button-no-aria-label
3
+ description: Icon-only button / clickable that relies on `title` for description without an aria-label
4
+ triggers:
5
+ files:
6
+ - "*.tsx"
7
+ - "*.jsx"
8
+ - "*.vue"
9
+ - "*.svelte"
10
+ - "*.astro"
11
+ - "**/*.tsx"
12
+ - "**/*.jsx"
13
+ - "**/*.vue"
14
+ - "**/*.svelte"
15
+ - "**/*.astro"
16
+ hunk_regex: "<button\\b|<a\\b|role=[\"']button[\"']|onClick=\\{"
17
+ confidence_floor: 0.75
18
+ ---
19
+
20
+ A clickable element renders only an icon (an `<svg>`, an emoji, a
21
+ single character from a font-icon set, an `<img>` whose `alt` is
22
+ empty or omitted) and depends on `title` to convey what it does.
23
+ `title` is hover-only and is NOT reliably announced by screen
24
+ readers, so the control is opaque to assistive-tech users: they
25
+ hear "button" with no context.
26
+
27
+ Flag when an interactive element added in this diff:
28
+ - has no readable text node as a child (only `<svg>`, an icon
29
+ component like `<ChevronIcon />`, `<img alt="">`, a single
30
+ glyph, or pure children-of-children that are themselves icons),
31
+ - has no `aria-label`, `aria-labelledby`, OR a visually-hidden
32
+ text label (`<span class="sr-only">…</span>`, `<VisuallyHidden>`,
33
+ Tailwind's `sr-only` utility), AND
34
+ - the element is `<button>`, `<a>`, `[role="button"]`, or has an
35
+ `onClick`/`onPointerDown`/`onKeyDown` handler.
36
+
37
+ The `title` attribute alone is NOT a fix; flag those too.
38
+
39
+ Suppress when:
40
+ - the button already has `aria-label`, `aria-labelledby`, or a
41
+ visually-hidden text child (`sr-only`, `visually-hidden`,
42
+ `<VisuallyHidden>`, `screen-reader-text`),
43
+ - the icon itself carries a label (`<svg aria-label="…">` or an
44
+ icon component whose own implementation injects an aria-label),
45
+ - the project already has a wrapping component like `<IconButton
46
+ label="…" />` that injects the aria-label,
47
+ - the element has visible text alongside the icon.
48
+
49
+ Severity: `minor` by default. Bump to `major` for primary CTAs
50
+ (submit / save / delete) where the missing label blocks the user's
51
+ intent, not just degrades it.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: array-iteration-missing-key-prop
3
+ description: .map() rendering JSX without a stable key prop (or using index as key for mutable lists)
4
+ triggers:
5
+ files:
6
+ - "*.tsx"
7
+ - "*.jsx"
8
+ - "**/*.tsx"
9
+ - "**/*.jsx"
10
+ hunk_regex: "\\.map\\s*\\("
11
+ confidence_floor: 0.7
12
+ ---
13
+
14
+ JSX rendered inside `.map(...)` either has no `key` prop or uses
15
+ the array index as a key. Without a stable key React can't reorder
16
+ children correctly: form inputs lose their internal state on
17
+ re-order, animations replay, and components mounted in the loop
18
+ double-fire effects when items shift positions.
19
+
20
+ Flag when:
21
+ - the `.map((item) => <Component .../>)` returns JSX with no `key`
22
+ - the `key` is `index` (or any expression that depends only on the
23
+ loop index) AND the underlying list can reorder / insert / remove
24
+
25
+ Suppress when:
26
+ - a stable id field is used (`key={item.id}` / `key={item.uuid}`)
27
+ - the list is provably static for the component's lifetime
28
+ (read-only constants, sorted-once display lists)
29
+ - the index IS the natural key (immutable, append-only)
30
+ - the rendered children are stateless / sideeffect-free and won't
31
+ notice a reorder
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: async-await-in-loop-n-plus-one
3
+ description: await inside a for / map / forEach, N round-trips per row
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: "for\\s*\\(|\\.(forEach|map|reduce)\\s*\\("
19
+ confidence_floor: 0.7
20
+ ---
21
+
22
+ A `for` loop / `for-of` / `.map(async ...)` / `.forEach(async ...)`
23
+ body contains an `await` against a network or database client.
24
+ Sequential awaits multiply latency by row count (100 rows × 30ms
25
+ = 3s) and turn a list view, batch import, or N-of-M lookup into a
26
+ classic N+1.
27
+
28
+ Flag when:
29
+ - the loop iterates over a list of identifiers / records and the
30
+ body awaits a fetch / db query / API call **per iteration**
31
+ - the result is collected into an array (a `Promise.all` over the
32
+ iterable would parallelize cleanly)
33
+
34
+ Suppress when:
35
+ - ordering matters AND the next iteration's input depends on the
36
+ previous iteration's result (rate-limited APIs, paged cursors,
37
+ workflows that build on prior steps)
38
+ - there's an explicit per-iteration delay / rate-limit
39
+ - the loop iterates a known-small fixed set (≤ 3) where
40
+ parallelization wouldn't change anything material
41
+ - a `.map(async ...)` is followed by `await Promise.all(...)`;
42
+ that IS the parallel pattern
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: async-click-double-fire-race
3
+ description: Async onClick handler with no in-flight guard / disabled toggle, double-clicks fire duplicate requests
4
+ triggers:
5
+ files:
6
+ - "*.tsx"
7
+ - "*.jsx"
8
+ - "*.vue"
9
+ - "*.svelte"
10
+ - "**/*.tsx"
11
+ - "**/*.jsx"
12
+ - "**/*.vue"
13
+ - "**/*.svelte"
14
+ hunk_regex: "onClick=|@click=|on:click=|onPress=|onPointerDown="
15
+ confidence_floor: 0.75
16
+ ---
17
+
18
+ A button / link / clickable triggers an async operation
19
+ (`fetch`, `mutate`, an awaited handler) without any guard against
20
+ re-firing while the first request is still in flight. A rapid
21
+ second click queues a duplicate request: a duplicate insert, a
22
+ double-charge, an interleaved state update where the late response
23
+ clobbers the early one. Touchpad users and slow networks hit this
24
+ constantly.
25
+
26
+ Flag when a handler added in this diff:
27
+ - is `async` OR returns a Promise / calls `await ...` / calls
28
+ `fetch(...)` / calls a project-specific async helper (mutate,
29
+ a fetch wrapper, etc.),
30
+ - AND has no `isLoading` / `inFlight` / `pending` boolean checked
31
+ at the top (early return when true),
32
+ - AND has no `disabled={isLoading}` (or equivalent class swap) on
33
+ the bound element,
34
+ - AND has no library-level dedup (TanStack Query's `mutate` with
35
+ the same key is OK; React Query's `useMutation` running through
36
+ its own queue is OK; a hand-rolled `setX([...])` is NOT).
37
+
38
+ Examples that should fire:
39
+ - `onClick={async () => { const r = await fetch(...); setState(r); }}`
40
+ with no disabled / loading guard
41
+ - an expand/collapse toggle that calls `fetchTeamMembers(id)`
42
+ but doesn't track `loadingTeams.has(id)`
43
+ - `<button onClick={async () => { await save(); navigate(...) }}>`
44
+ where save can take seconds
45
+
46
+ Suppress when:
47
+ - the handler is wrapped by `useMutation` / `useAction` /
48
+ `useTransition` / similar library primitive that owns the
49
+ in-flight tracking,
50
+ - the bound element has `disabled={isLoading}` /
51
+ `aria-disabled={isLoading}` / a CSS class swap that prevents
52
+ pointer events,
53
+ - the handler is genuinely idempotent (PATCH against a known
54
+ resource state, GET-only, etc.); call out the idempotency
55
+ guarantee when you suppress.
56
+
57
+ Severity: `minor` for read-only / idempotent paths, `major` for
58
+ writes / mutations / billable actions (the duplicate insert and
59
+ the double-charge cases).
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: async-floating-promise
3
+ description: An async function is called without await / .catch, silent rejection
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: "\\basync\\b|\\.then\\("
19
+ confidence_floor: 0.7
20
+ ---
21
+
22
+ An async function is invoked from synchronous code (or from another
23
+ async fn) without `await`, without `.then(...).catch(...)`, and
24
+ without `void`. The returned promise floats: a rejection becomes an
25
+ unhandled rejection (process warning, eventual crash on newer Node
26
+ versions, swallowed entirely in browsers) and the caller has no
27
+ ordering guarantee with subsequent statements.
28
+
29
+ Flag a call to an `async fn` (or a fn that demonstrably returns a
30
+ promise: `.then` chains, fetch wrappers, db client calls) whose
31
+ return value is **discarded**: not assigned, not returned, not
32
+ awaited, not chained with `.catch`, and not preceded by the explicit
33
+ `void` keyword.
34
+
35
+ Suppress when:
36
+ - the call is intentionally fire-and-forget AND prefixed with `void`
37
+ (the documented "I know" marker)
38
+ - the outer scope wraps in `Promise.all([...])` / `Promise.allSettled`
39
+ - the call is on a logger / telemetry / metrics method where dropped
40
+ rejections are explicitly acceptable