Blacksmith has no released versions. It runs from a clone and the only
supported code is the current main. Fixes land on main; there are no
backport branches.
Do not open a public issue.
Use GitHub's private vulnerability reporting: the repository's Security tab → Report a vulnerability. That opens a private advisory visible only to you and the maintainers.
Please include:
- what an attacker can do, not only what looks wrong;
- the smallest reproduction you have — a command, a spec file, an event payload;
- the commit SHA you saw it on;
- whether it needs the factory to be running an epic, or only a checkout.
You should get an acknowledgement within a few days. This is a small project — there is no bounty and no SLA, and a fix may take longer than an acknowledgement does. If a report is accepted, the advisory is where the fix and the disclosure timing get agreed.
Never put a credential in a report. If a report needs one to make sense, say so and it will be handled in the advisory thread.
This is an orchestrator that runs language-model agents against a git checkout. The interesting attack surface is the boundary between untrusted text and things that execute:
- Prompt injection through fetched or quoted text.
bs prompt wrapexists to fence such text as data before it reaches a prompt (wrapIngested,factory/orchestrator/src/provenance.ts). A path where untrusted text reaches a dispatch unfenced is in scope. - Escaping the worktree. Workers are confined to
<project-parent>/.wt/<project>/<task-id>and to their spec's path claims. A way for a task to write outside its claims without failing the gate is in scope. - Bypassing a gate. Anything that lets a task reach the merge queue with a failing schema check, a failing test gate, or an unresolved S1/S2 finding.
- Judge integrity. A read-only judge that can mutate the tree it is
judging, reach the network, or influence its own verdict. The role sandbox
(
bs sandbox, plus thejudge-*rules infactory/policies/guardrails.yml) refuses those commands up front, andbs worktree fingerprint/verifybracket the run to catch what a text matcher cannot see. Both halves are stated indocs/standards/guardrails.md"The role sandbox"; a way past both is in scope. - Grading one's own work. A tester that can edit the implementation it is
covering, so a red test turns green by changing the subject rather than the
test.
role_write_scopesin the same file fences it to test files; a write it reaches outside them is in scope. - Secret leakage. A credential value reaching the event log, a projection,
a PR body, a screenshot, or a judge prompt. See
docs/standards/guardrails.md"No secrets in outputs". - The guard hook.
.claude/hooks/guard.sh— a transport shim overdist/policyHook.js, the same decision asbs policy hook— and the deny list in.claude/settings.jsonblock pushes tomain, force-pushes, history rewriting, and unbounded deletion. A bypass is in scope, and so is a shim that fails open: when the policy layer cannot answer, the hook escalates to the operator and never emits anallowenvelope, because a hook'sallowoutranks the operator's own deny list. - The local dashboard.
bs ui servebinds a local HTTP server with write routes (waivers, lesson approve/reject). Anything that turns it into a remote-write surface is in scope.
- The models themselves. A model producing wrong or low-quality output is a bug, not a vulnerability. That is what the gates are for.
- Provider accounts and keys. Codex and DeepSeek credentials are yours; their handling inside this repo is in scope, their security is not.
- Anything requiring an operator to approve it. Deploys, outbound sends,
and merges to
mainare deliberately gated on a human. "The operator could approve something bad" is the design, not a flaw. - Running the factory against a repository you do not trust. Blacksmith executes that repository's own test commands.
.vuesingle-file components are not type-checked.vue-tscneeds Volar, Volar needs TypeScript's classic Node compiler API, and this repo runs TypeScript 7's native port, which does not expose one. The hole is narrower than it was:ui/test/vueContract.test.tscompiles every SFC with Vue's own compiler and fails the gate on anything the compiler had to leave for runtime — an identifier no binding provides, an unregistered component or directive, an import the template does not use — and on any call site passing a prop, event or slot the component it calls never declared. That is a contract check, not a type check: it knows a prop exists, not that astringwas passed where anumberwas declared. The rest of the mitigation is structural — UI logic lives inui/src/lib/*.ts, which is type-checked, linted and unit-tested, and the SFCs stay thin.- Per-task budget caps are advisory.
bs wave checkrefuses to admit a wave whose declared cost would cross the epic'scap_tokens, so the epic cap infactory/policies/budgets.ymlis enforced before any of that wave's tokens are spent. The per-task caps are not, and deliberately: a cap a worker polices mid-task is pressure on the work it is measuring (D-29). Nothing hard-stops a session already running. - CI is one Linux runner. No matrix — see
INSTALL.md§ Known platform gaps.
- All credentials live in environment variables.
.envis gitignored;.env.exampleis the only committed env file and carries variable names only. - The CLI loads
.envfrom the repo root at start, into its own process only (factory/orchestrator/src/dotenv.ts). It sets names the environment does not already hold and never overrides an exported one, so a runner's real key always beats a developer's file; it returns the names it set and never a value. There is still no dotenv dependency and no file is ever written. - The secret scan allowlists exactly one path,
^\.env$, because that file holds real keys on purpose and.gitignoremeans it can never be committed. The allowlist is anchored —.env.exampleand.env.localare still scanned — anddotenv.test.tsasksgit check-ignorewhether the reason still holds, so it cannot outlive the ignore that justifies it. factory/policies/crosscheck.ymlreferences provider key env var names, never values.- Credential-shaped strings that appear in the test suite are deliberately synthetic fixtures named inside acceptance criteria. They are not live credentials, and they must stay synthetic.