Do not open a public issue for a security problem.
Report privately through GitHub's private vulnerability reporting — the Security → Report a vulnerability tab on this repository. That channel is preferred because it keeps the report, the fix, and the advisory in one place.
If private reporting is unavailable to you, email araszkiewiczrafal@gmail.com with
ai-badger security in the subject.
- What an attacker can do, and what they need in order to do it (a hostile catalog entry? a
hand-edited
.ai-badger/config.json? a malicious target repo?). - The affected file and, where possible, a failing test or a reproduction command.
- The version —
VERSIONat the repo root, orframeworkVersionin the target repo's.ai-badger/manifest.json.
This is a one-maintainer project. There is no on-call rotation and no guaranteed response
time. Realistically: acknowledgement within a week, and a fix released as a normal version bump
with a docs/changelog/ entry describing it. If a report needs coordinated disclosure, say so
and the advisory stays private until a fixed version is tagged.
Only the latest tagged release is supported. Releases are tagged ai-badger--v{version};
the baseline restarts at ai-badger--v0.20.0 (versions 0.3.0–0.19.0 carry no tag and never
will — see RELEASING.md).
Fixes ship forward. There are no backports to older minors.
ai-badger is a local code generator. There is no server, no hosted service, and no account. The scripts read a catalog from this repository and write files into a target repository and, for some agents, into the user's home directory. So the honest threat model is:
| Surface | What it does | Who controls the input |
|---|---|---|
welcome-ai-badger / den-refresh scaffolding |
Writes .ai-badger/, agent-discovery files (CLAUDE.md, .github/copilot-instructions.md, HERMES.md), and merges into .claude/settings.json / .mcp.json |
The framework catalog plus the user's own .ai-badger/config.json |
| Hermes skill discovery | Creates symlinks under ~/.hermes/skills/<project>/ and copies hook plugins into ~/.hermes/plugins/ |
Same |
| Skill / dependency installation | Can run pip, npm, and plugin-install commands |
Catalog templates; opt-in only, see below |
feed-badger |
Takes content out of a private repo and puts it in a public draft PR | The user's repo |
| Generated pages | owner-gate-review and evidence-first-research write HTML built from repository content |
The document being reviewed or researched |
| Hooks | Run in the agent's process on session start, prompt submit, and tool calls | Catalog scripts, plus project files they read |
The dangerous direction is outbound and side-effecting, not inbound parsing. A vulnerability report that lands hardest here is one where scaffolding writes outside the target repo, executes something the user did not approve, or publishes something the user did not intend to publish.
These are the measures that exist today, so a report can build on them rather than restate them. Each links to the release that introduced it.
From 0.24.0 — outbound scan:
feed-badger'sopen_pr.pyscans every declared path for credential-shaped literals before running any git command, and exits non-zero on a finding (since 0.172.15 it scans the staged set instead, see below). The matched text is never logged — findings carry only{file, pattern}withpatternfrom a closed vocabulary.git add -Awas removed. Contributions are staged from an explicit, required, repeatable--path, so unrelated dirty files in the checkout cannot be published by accident.- The scanner is one module (
engine/unsafe_literals.py) used by both the inbound learned-skills path and the outbound PR path.
From 0.25.0 — the hardening pass:
- No shell. Install commands are argv lists, not strings passed to
shell=True. The command template is tokenised before substitution, so a value containing a space or;stays one argument. - No silent installs.
run_dependency_checkreports what would be installed and where;--executeopts in. This is why 0.25.0 is inBREAKING_VERSIONS. - ReDoS caps. The two
.mjsgate scripts refuse patterns over 500 characters and files over 1 MB, and report the refusal through the normal error path. - User-scope state is owner-only.
~/.claude/awm/state.json,decisions.jsonl, and the prompt-marker state are0600(directory0700); the decision log is capped at 5,000 lines. Thecall-behavioristaudit log at~/.ai-badger/debug/gets the same treatment and the same cap — it records project paths, session ids and working directories, so it says where someone works and what ran, and it logs no tool input and no prompt text at all. - Path containment is asserted, not assumed.
project.namecannot escape~/.hermes/skills/<name>, and a manifesttargetcannot steer the drift hasher out of the project (an absolute right-hand side no longer wins thepathlibjoin).
From 0.69.0 — a task owns its worktree:
- A task id cannot escape the worktree directory.
task_tracker.pysplices its<taskId>argument into.ai-badger/worktrees/<taskId>and then hands that path togit worktree add/remove. A..or a path separator would have pointed a removal somewhere nobody asked for.worktree_path()refuses those ids rather than sanitising them: a silently rewritten id would no longer match the one in the tracking JSON, which turns a containment fix into a correctness bug.
From 0.70.0 — a finding carries how it is known:
- A generated page reaches no network.
evidence-first-research's renderer emits inline SVG and inlined CSS with no<script src>, no CDN, no fonts and no fetch — the page contains nohttp://orhttps://string at all. A report is built from repository content and then opened in a browser, so anything it could fetch is an exfiltration path out of a machine that has the repository on it. - It refuses to write inside the repository. The markdown record is the artefact and is committed; the HTML view goes to a temp directory. A generated page committed beside its source is both a second source of truth and a thing that gets published without being reviewed as published content — which is exactly how the home-directory leak below happened.
From 0.69.1 — a shipped page is not prose:
- A shipped page cannot carry a machine-specific path. Generated HTML committed under
docs/had a reviewer's home directory baked into a config field. That is low-severity information disclosure — it names a user and their directory layout in a public repository — and it is the kind that arrives by generation rather than by typing.shipped_paths_guard.pyfails the build on/Users/,/home/, orC:\\Users\\in a tracked page.docs/andtests/stay exempt for prose and fixtures; the exemption stops at generated pages, which are neither.
From 0.172.15 — outbound staging and journal restore:
open_pr.pyscans what it stages, not what it was told. The 0.24.0 scan read the declared paths whilegit addexpanded them. A--path 'features/**'was one missing file to the scanner and every match to git, andgit commitalso took whatever was already staged. Paths are now staged withGIT_LITERAL_PATHSPECS=1, into an index that must be clean beforehand. The scan reads exactlygit diff --cached --name-only, and a finding unstages and refuses.- The credential scan has a size cap, and the outbound path refuses what it skips.
engine/unsafe_literals.pyskips any file overLITERAL_SCAN_MAX_BYTES(1 MB) and reports no finding for it.open_pr.pyrefuses such a file instead of publishing it unscanned. The one caller that still lets an oversized file through is the inbound Hermes sync,sync_skillinfeatures/common/hooks/learned_skills_sync.py:347: it copies a learned skill into the user's own project, not out of it. red_proof.pyrestores a stale journal only onto the file it mutated. The journal records the sha256 of both the original and the mutated content. A restore writes only a file inside the repo that still holds one of the two. A path outside the repo, a file edited since the crash, or a journal withoutmutated_sha256exits 3 and writes nothing.
Structural properties that predate those waves:
- Root resolution never touches the network by default —
badger_lib.ensure_root(allow_network=…)is an explicit opt-in, asserted bytests/test_find_root_never_touches_the_network. - Writes are atomic —
badger_lib.atomic_write_textusesos.replace, and a failed scaffold leaves a detectablemanifest.json.partialmarker rather than a half-written manifest. - Destructive writes are guarded — a scaffold refuses to clobber files it does not own:
unparseable
.claude/settings.jsonaborts rather than being overwritten, a foreign entry in the Hermes namespace is never removed, and skills containing symlinks are refused on sync. - No hand-rolled crypto and no hardcoded secrets — both are non-negotiable invariants in
CLAUDE.md, enforced by review rather than by a scanner.
- CodeQL runs on every push to
main, every pull request, and weekly (.github/workflows/codeql.yml). - Dependabot watches
engine/requirements.txtand the GitHub Actions used by the workflows, weekly (.github/dependabot.yml). - gitleaks scans the commit range on every pull request against
mainand every push tomain, and the whole history weekly (.github/workflows/secret-scan.yml). Since 0.91.0 thegitleakscontext is a required status check on the default-branch ruleset — before that the job could be red and a PR still merge. Findings are redacted: this repository is public, so an unredacted report would publish the credential it just caught. deps_guard.pyfails the build on a third-party import thatengine/requirements.txtdoes not declare — in pre-commit and on all three CI Python versions.- The runtime dependency surface is deliberately tiny:
jsonschemaandpyyaml. Everything else is Python 3.10+ stdlib. Their imports differ on purpose —jsonschemais required, and although it is imported lazily (inside the validation functions, so the entry points that never validate do not pay for it) theImportErroris never swallowed, because a guarded import would let validation silently pass when it is absent;pyyamlis optional and guarded, degrading to a printed note.
There is still no secret-scanning pre-commit hook. That is now a decision rather than an omission, and both candidates were looked at:
- gitleaks as a local hook.
.pre-commit-config.yamlis deliberately onelocalrepo oflanguage: systemhooks with no third-party dependency. Adding gitleaks there puts a downloaded binary or a Go toolchain on every contributor's machine and in every commit — the trade CI can make and a commit hook cannot, which is why.github/workflows/secret-scan.ymluses the Action and stops there. engine/unsafe_literals.pyas a local hook. It is offline and stdlib-only, but it is a five-pattern guard on content leaving a repository, not a history scanner — a different door on purpose. It also matches a tracked file on a deliberately fake fixture value (tests/test_learned_skills_sync.py), so it would ship with a permanent exception from its first run. A permanently-baselined scanner is theatre, and a five-pattern one advertised as secret scanning is worse than none: it buys false confidence.
So, stated plainly: a credential can be committed locally, and is caught when the branch is
opened as a pull request or merged to main — secret-scan.yml triggers on
pull_request: branches: [main] and push: branches: [main]. A branch pushed and never opened
as a PR is not scanned until it merges. That gap is deliberate, and the workflow says so in its
own comment: pull_request is the trigger a required status check is evaluated on and the only
one that reaches a fork, so it is the one kept.
This paragraph has now been wrong in both directions, which is worth recording. It first said a
topic branch was scanned only once it reached main or a PR; that was corrected to
push: branches: ['**'], which was true at the time. 0.91.0 then made gitleaks a required
check and narrowed push to main to stop running the scan twice per PR — and this paragraph
kept advertising the wider trigger. Verify the trigger in
secret-scan.yml before relying on this sentence; a
security document that overstates its own coverage is the dangerous direction to be wrong in.
- Vulnerabilities in the coding agents themselves (Claude Code, GitHub Copilot, Hermes Agent) — report those to their vendors.
- Vulnerabilities in bundled external MCP tools such as code-review-graph — report upstream. What is in scope here is how ai-badger declares, merges, or installs them.
- The fact that scaffolding writes files into your repository and home directory. That is the product. A report that it writes somewhere it should not is very much in scope.
- Anything requiring you to already have write access to this repository's catalog or to the user's machine.