Custom scanners
You or your agent can configure a compatible scanner from GitHub. Then you approve it before it runs.
A finding is one problem at one place in the code, with a severity. A custom scanner's findings join the same report as the built-in scanners' findings. OpenQodex keeps the findings on changed lines. It drops findings in test fixtures and in rules you switched off.
A custom scanner is a command that runs on your machine with your permissions. scan and review never download or install a custom scanner. Only openqodex trust does that. OpenQodex runs a custom scanner only after you approve its exact entry.
What makes a scanner compatible
- It writes SARIF, a standard JSON format for scanner results. Or it writes one JSON document, which a
mapblock turns into findings. - OpenQodex cannot read plain text, HTML or JSON Lines.
- It installs in one of four ways:
- a release asset for your system and CPU;
- an exact npm version;
- an exact PyPI version;
- a program on your
PATH.
- OpenQodex runs it only when
~/.openqodex/trust.jsonon that machine holds your approval of that exact entry. A fresh GitHub-hosted runner holds none, so the GitHub Action skips it there.
Add one
Add an entry to .openqodex/config.yaml in your repository. This entry adds Bearer CLI, which writes SARIF:
scanners:
custom:
- source: https://github.com/Bearer/bearer
run: bearer scan {target} --format sarif --output {report} --quiet --disable-version-check
target: repo
source: the scanner's GitHub repository.run: the command line. OpenQodex splits it into words. It starts the first word as the program. It uses no shell.
OpenQodex fills in three placeholders:
{report}: the file that the scanner writes its report to. With no{report}inrun, OpenQodex reads the report from the scanner's standard output.{target}: by default, the changed files. Withtarget: repo, the repository root.{repo}: the repository root.
config lists the optional keys: name, version, format, map, paths, target, timeout_seconds and install.
Bearer CLI is under the Elastic License 2.0, which is not an open source licence. Check the licence of each scanner you add.
Add one through your agent
Say to your agent: "add https://github.com/Bearer/bearer to OpenQodex as a custom scanner".
- The agent reads the scanner's README for its SARIF or JSON flag.
- The agent writes the entry in
.openqodex/config.yaml. - The agent asks you to approve the entry. The openqodex skill tells it never to run
openqodex trustwithout asking you first. - After your approval, the agent runs
~/.openqodex/bin/openqodex scan --only custom:<name>. Then it tells you what the scanner found.
for-agents gives agents the same steps.
Approve it
npx openqodex trust
trust handles each entry that is new or changed since your last approval. What it does depends on the install form.
For a release asset, the default form:
trustreads the release from the GitHub API. Withoutversion, it takes the latest release.- It picks the release asset for your system and CPU.
- It downloads the asset into a quarantine folder. It does not unpack the asset onto the tool path, and it does not run it.
- It checks the asset against the project's checksum file, when the project publishes one.
- It prints what will run. Then it asks yes or no.
For install: path, trust downloads nothing. It records the sha256 of the program on your PATH, prints what will run, and asks.
For install: { npm: ... } or install: { uv: ... }, trust downloads nothing before it asks. It installs the exact version only after your yes.
The printout shows the name, the source, the version and the install form. It shows the asset, the download URL and the sha256. It also shows the checksum check, the program path, the run line, the paths and the target. Read it before you answer.
On yes, trust installs the scanner and stores your approval. On no, OpenQodex skips the scanner until you approve it.
How trust picks a release asset
trust matches words in the file name of each release asset:
- system:
darwin,macos,osx,appleorlinux; - CPU:
arm64,aarch64,x86_64,amd64,x64or64bit; - format:
.tar.gz,.tar.xz,.zip, or a bare program.
When no release asset matches, trust stops. When more than one matches, trust also stops. It prints the matching release assets and the line to add.
Name the release asset by hand. {version} stands for the version. {os} and {arch} stand for the system and CPU words above. So one line fits each supported system: macOS and Linux, on arm64 and x64.
- source: https://github.com/Bearer/bearer
run: bearer scan {target} --format sarif --output {report} --quiet --disable-version-check
target: repo
install:
asset: "bearer_{version}_{os}_{arch}.tar.gz"
The name must match exactly one release asset.
The other install forms:
install: path: OpenQodex uses the program on yourPATH. When that program changes, OpenQodex skips the scanner until you approve it again.install: { npm: <package@1.2.3> }: OpenQodex installs from npm. The spec must name an exact version.install: { uv: <package==1.2.3> }: OpenQodex installs from PyPI through uv. The spec must name an exact version.
What the stored hash means
trust records the sha256 of the release asset it downloaded.
Some projects publish a checksum file. When the release asset matched it, trust checked the hash against upstream. Otherwise, the stored hash is only the hash of your first download. This is trust on first use: it does not prove that the first download was genuine. The printout says which of the two applies.
Where approvals are stored
trust stores approvals in ~/.openqodex/trust.json. It keys each one by the repository root and a hash of the entry and the resolved release asset. An approval in one repository does not cover another.
An edit to the entry changes its hash. OpenQodex then skips the scanner as untrusted until you run openqodex trust again.
openqodex trust --listshows each custom scanner and its state.openqodex trust --revoke <name>removes one approval.openqodex doctoralso shows the state of each custom scanner.
Report formats
SARIF
SARIF is a standard JSON format for scanner results. Many scanners write it when you pass a flag. It is the default format.
OpenQodex reads the rule id, the file, the start and end line, and the message of each result. A scanner severity is one of critical, high, medium, low and info. OpenQodex takes it from a security-severity score. A score on the result wins over a score on its rule:
- 9 or more: critical
- 7 or more: high
- 4 or more: medium
- above 0: low
- 0: info
Without a score, the SARIF level decides. error is high, warning is medium, note is low and none is info. Any other level is medium.
The report uses its own scale: critical, major, minor, nitpick and info. OpenQodex maps critical to critical, high to major, medium to minor, low to nitpick and info to info.
Worked example: Bearer CLI with SARIF
Bearer CLI checks code for security and privacy problems, such as a weak hash.
scanners:
custom:
- source: https://github.com/Bearer/bearer
run: bearer scan {target} --format sarif --output {report} --quiet --disable-version-check
target: repo
target: repopasses the repository root, becausebearer scanscans a folder.--disable-version-checkstops Bearer from asking for a newer release. Check what each scanner you add sends before you approve it.--quietstops Bearer from printing what its help calls non-essential messages.- Findings appear in the report as
custom:bearer:<rule id>, such ascustom:bearer:javascript_lang_weak_hash_md5.
This example was tested on 2026-10-12 on an Apple Silicon Mac. trust picked bearer_2.1.1_darwin_arm64.tar.gz. It checked that release asset against the project's checksums.txt. The test then added a JavaScript file that used MD5 to the demo repository. scan --only custom:bearer reported javascript_lang_weak_hash_md5 on the changed line.
json-map
Use json-map for a scanner that cannot write SARIF. OpenQodex reads its JSON report through a map block. Each value is a dotted path into the report, with [n] for a list index. items points at the list of results. OpenQodex reads the other paths from each result.
Example: gosec with json-map
gosec checks Go code for security problems. This example shows the shape of a map block; no test runs it. gosec's JSON report looks like this:
{
"Issues": [
{ "severity": "HIGH", "rule_id": "G101", "details": "Potential hardcoded credentials",
"file": "/home/me/app/config.go", "line": "12" }
]
}
The entry:
scanners:
custom:
- source: https://github.com/securego/gosec
run: gosec -fmt json -out {report} -no-fail {repo}/...
paths: ["**/*.go", "*.go"]
format: json-map
map:
items: Issues
file: file
line: line
rule: rule_id
severity: severity
message: details
severity_map:
HIGH: high
MEDIUM: medium
LOW: low
items: Issuespoints at the list.filemay be absolute. OpenQodex turns a path inside the repository into a repository path.linemay be a number or a string of digits. OpenQodex skips a result that has no file or no line.severity_mapturns gosec's words into the scanner scale. OpenQodex reads a word that is not in the map as medium.-no-failkeeps gosec from exiting with an error when it finds something.
The end-to-end test of custom scanners runs actionlint from its GitHub release with json-map (tests/e2e/custom-scanner.test.ts).
From docs/custom-scanners.md at 68ef6b8.