Skip to content

v0.33.2

Latest

Choose a tag to compare

@github-actions github-actions released this 18 Sep 03:53
· 461 commits to main since this release

v0.33.1 was tagged and never published; the publish job now hands npm a file path

v0.33.1 was tagged on 2026-09-16 and never reached npm. Its release run
(35161269506)
built and reviewed the tarball, then the publish job ran
npm publish "release-pack/<tarball>". npm reads a bare directory/name as
the GitHub shorthand <owner>/<repo>: it ran
git ls-remote ssh://git@github.com/release-pack/<tarball>.git (no such
repository exists), which failed with Permission denied (publickey), and
exited 128 before any registry request. npm view hackmyagent@0.33.1 prints
E404. The tag stays where it is and has no GitHub release; 0.33.2 is the same
code with the version number changed, released through the fixed workflow. It
was cut on the branch release/0.33.2 from the v0.33.1 commit, with the
workflow fix, the changelog and the version bump on top. main requires
linear history, so the commit v0.33.2 points to is reachable from the tag and
not from main: git branch --contains v0.33.2 does not list main.

release.yml now publishes ./release-pack/<tarball>, which npm's parser
types as a file. The shape test (HMA-40.AC1 in
__tests__/repo/release-workflow-shape.test.ts) asserted the bare form
verbatim, so it passed at the tag. It now requires the argument to start with
./ or / and types it through npm's own package-spec parser, loaded from
the npm on PATH, with the bare form as the positive control (it must read as
git). If that parser cannot be loaded the test fails and names the path it
expected; it never skips. test-matrix.yml also gains a workflow_dispatch
trigger, so the test suite can be run on the exact commit a release tag points
to, not only on pull requests and pushes to main.

The release is reviewed as the CI-packed tarball, never the tree (HMA-40)

release.yml no longer grants contents: write and id-token: write to one
job that also ran npm ci and npm test with them in hand. It is now four
jobs joined by needs:: build (contents: read, npm ci --ignore-scripts,
build, test, npm pack, sha256 recorded and the tarball uploaded), review
(runs scripts/release-artifact-review.mjs against the downloaded artifact),
publish (id-token: write and nothing else, no checkout, publishes the
digest-checked tarball with --provenance), and verify (SLSA v1 predicate
present and npm's dist.integrity equal to the sha512 of the reviewed bytes).

The review script refuses: entries outside the files allowlist, dotfiles,
test-shaped paths, install-time scripts, caret/tilde ranges on @opena2a/*
or aim-sdk, a CLI that cannot run --version/--help/secure --ci from a
clean global prefix with an empty HOME and the network cut, a shipped scanner
that misses a planted credential control, an integrity self-check that is not
live in the shipped tarball, and high-or-above npm audit --omit=dev
advisories. A check that cannot run is an error, never a pass, and every
check appears in the census line on every outcome.

Fallout absorbed in this change: the integrity manifest the CLI verifies
itself against moves from dist/.integrity-manifest.json to
dist/integrity-manifest.json (no leading dot — the review refuses dotfile
entries). Already-installed versions are unaffected: each installed version
reads only its own dist/, so no existing install ever looks for the new
path. No documented interface and no external consumer reads the manifest
path; it is internal to the CLI's startup self-check. And the release review
now proves that self-check is live in the shipped tarball rather than
assuming it: the self-check-live check requires the manifest to cover
exactly the packed dist/ files at the packed version, then corrupts
dist/index.js in a scratch install and requires hackmyagent --version to
quarantine (exit 3, INTEGRITY CHECK FAILED) — so a manifest that goes
missing under any future rename is a named release failure, never a silent
dev-mode CLEAN. Separately, the three floated @opena2a/* dependency ranges
are pinned to their locked versions, and every workflow job that runs
npm ci now refuses, before its install, a tree that tracks a .npmrc /
.yarnrc(.yml) / .pnpmfile.cjs / .envrc.

The Gate file approval predicate now covers the credential-boundary paths

The path-scoped merge gate in .github/workflows/gate-file-approval.yml gated
exactly ^.github/. Its changed-file predicate now also classifies four
credential-boundary sources as gate files requiring an approving review on the
pull request's current head commit:
src/nanomind-core/analyzers/credential-analyzer.ts,
src/nanomind-core/compiler/semantic-compiler.ts,
src/nanomind-core/security/defense-in-depth.ts (the report-redaction module)
and src/nanomind-core/analyzers/stego-analyzer.ts. Each file pattern is
anchored at both ends, so a lookalike path above, below or beside a gated one
does not gate, and a pull request touching neither class still passes
immediately.

The workflow is restructured so every API read happens before a
marker-delimited, pure-shell verdict block, and a new suite,
__tests__/gate/gate-file-verdict.test.ts, lifts that block out of the YAML
and executes it verbatim against planted changed-file lists: each boundary
path evaluates to the blocking action_required conclusion absent approval on
the current head, a change touching no gate file evaluates to success ("no
gate files touched"), and over every no-approval input the conclusion
vocabulary is exactly action_required or failure — never success, and
never neutral, which required checks treat as passing.

scan <host> tells a live host apart from an unreachable one, and finishes

hackmyagent scan https://opena2a.org reported a site that answers on 443 as
Target unreachable, score -1/100, in 34 ms; hackmyagent scan opena2a.org
ended with Scan timed out after 4000ms (advertised-command audit,
2026-09-13). Two defects. The raw target string was handed to socket.connect
as the DNS name, so a URL failed on every port before a packet left the
machine. And the scan's global budget was ports x timeout, which covered the
port probes and not the seventeen sequential HTTP requests that followed on
each open port, so any host that opened both default ports outran it.

The target is parsed first (parseTarget: a URL scans its hostname, and the
port it names when it names one; -p still wins). Each TCP connect now keeps
its outcome (open, closed, filtered, unresolved, error), the HTTP
probe categories run side by side with an end-to-end bound per request, and
the budget counts every phase. The result carries hostReachable and
portStates, the text report prints a Host: line, and the no-open-port case
says which of three states it is in: the name did not resolve
(SCAN-UNREACHABLE), nothing answered on the scanned ports nor on 443/80
(SCAN-UNREACHABLE), or the host answered and no scanned port was open
(SCAN-NO-OPEN-PORTS, new, score N/A like its sibling). A refused connection
counts as an answer: the host is there, the port is not.

Two probes that the stall had been hiding are tightened in the same change:
MCP-TOOLS fired on any 200 whose body contained the word "tools", so a site
with an HTML /tools page was reported as exposing an MCP tools endpoint (a
CRITICAL on a marketing page); it now requires a JSON listing with a tools
member. CLAUDE-MD-EXPOSED fired on any 200 at /CLAUDE.md, which a
single-page app's HTML fallback satisfies; an HTML body no longer counts.

red-team no longer echoes the credentials it read, in either output mode

hackmyagent red-team ./.mcp.json --json placed the config's first long line
into target.declaredPurpose verbatim; for a typical MCP config that is the
postgresql://user:PLACEHOLDER@host/db connection string, password included, and
depending on the artifact the same text reached capabilities,
modalStatements, the surface map and the generated payloads
(advertised-command audit, 2026-09-13). The text output carried less of it:
through 0.25.2 the vulnerability block embedded the declared purpose in a
payload description, and from 0.26.0 each Stated rule: surface line printed
the first 80 characters of an artifact sentence, credential included when one
sat inside them. The reader
now redacts the whole artifact at the report boundary before any extraction,
the order NanoMind's extractDeclaredPurpose already uses, plus one rule the
boundary did not carry: userinfo in a URL of any scheme becomes
scheme://[REDACTED_URL_CREDENTIAL]@host, the host kept because the surface
map is about it. Four more carriers the shared boundary does not catch on an
MCP config, a SKILL.md or a system prompt are masked in the same place: a
quoted value under a credential-named key in JSON spelling
("PGPASSWORD": "..."), an unquoted value after such a key
(export DB_PASSWORD=..., YAML password: ..., a X-Api-Key: ... header
line), an Authorization: Bearer ... header value, and a Password=...;
DSN field.
Every rule is bounded, so a large artifact cannot make the reader hang. The
profile records redaction: { status, shapes } so a consumer can tell that
content was cut, and the JSON now goes through the shared stdout chokepoint,
so it carries hackmyagentVersion: output without that key came from a
version that echoed.

The root help names the contribution control that exists

The telemetry block on --help said contribution could be disabled with
hackmyagent telemetry off. That command toggles usage telemetry in
~/.config/opena2a/telemetry.json and never read the contribution record,
which lives in ~/.opena2a/config.json under contribute.enabled
(advertised-command audit, 2026-09-13, found on ai-trust's identical line).
The block now names --no-contribute and that key, and says what
telemetry off covers.

secure --ci -b oasb-1 control 5.1 reads the credential files the scanner reads

OASB-1 control 5.1 "No Hardcoded Credentials" mapped CRED-002, CRED-003,
CRED-004 and SEM-CRED-001 to SEM-CRED-004, but not CRED-001, the
plaintext-credential walk that reads .claude/settings.json, .env and the
MCP and agent configs. The control therefore passed on a tree the plain scan
flags. It now consumes CRED-001, and the evidence line cites the record's
file and line through the same path escape the plain scan uses
(CRED-001: Anthropic API Key found in plaintext (.claude/settings.json:1));
oasb-2 goes through the same report and is fixed with it. On such a tree the
benchmark now reports [-] 5.1 and Credential Protection 0/1 where 0.33.0
reported [+] 5.1 and 1/1. The exit code moves only on a tree that passes
every other control: a developer tree holding CLAUDE.md, an .env.example
of placeholder values, package.json, package-lock.json and LICENSE
beside the key went from Certified, 100% (9/9), exit 0 on 0.33.0 to Needs
Improvement, 89% (8/9), exit 1; a directory holding nothing but
.claude/settings.json exits 1 on both versions, because other controls
already fail there (Not Passing, 43% on 0.33.0 and 29% on 0.33.2). The SARIF
join matches evidence lines by their checkId prefix instead of a substring
(#739).
check <dir> --offline still runs none of the static checks on a local
directory (#740),
-v after secure, scan-soul or check still prints the version and exits
0 without scanning (#741),
and DEP-001 still fires on a tree without a package.json
(#751); all three are
unchanged from 0.33.0, listed under its Known issues, and target 0.34.0.

Security

red-team in 0.11.14 through 0.33.0, every published version with the command,
copied text from the scanned artifact into its output unredacted: with --json,
target.declaredPurpose, target.capabilities, target.modalStatements,
target.vulnerabilitySurface[].surface and results[].payloadInput; in text
mode, the vulnerability block through 0.25.2 and the Stated rule: surface
lines from 0.26.0. Fixed here: the artifact is redacted before extraction, and
red-team --json carries a top-level hackmyagentVersion; output without that
key came from an affected version. If output from an artifact holding a
credential left your machine, rotate the credential. red-team never uploads
its output; it travelled only where you moved it.
Separately, GHSA-ccp3-g7fv-9cqr
("Credential characters could reach JSON output and be marked as checked", affected
range >= 0.17.11) remains open in 0.33.2, and no published version resolves it. The
advisory's guidance applies to 0.33.2 unchanged: search stored JSON and ASFF output for
the vendor prefix of any key present in a scanned target, for example
grep -rn 'AKIA' path/to/stored-output, and if characters of the key follow the prefix,
rotate that key and remove the stored file wherever it travelled, including CI artifacts,
log pipelines and tickets. A clean secure result is not evidence that no credential is
present; keep a dedicated secret scanner in the path.

The MCP checks read every root config spelling, so renaming mcp.json no longer raises the rating

The deterministic MCP checks (MCP-001 to MCP-010, NET-001, NET-002,
TOOL-001 to TOOL-003) read only a root mcp.json. .mcp.json — the
project-scope file Claude Code reads — was outside their discovery set, so a
tree whose servers lived there scored higher than the same servers in
mcp.json: OASB-1 controls 2.3, 4.1 and 5.2 read not-applicable while the
servers were live (33% as mcp.json, 36% as .mcp.json on the tracking
issue's fixture). Discovery is now the exported ROOT_MCP_CONFIG_FILES
constant (mcp.json, .mcp.json), consumed by every root read site and
every list that names the root config, and pinned in both directions by a
source contract test. Every spelling that exists is evaluated on its own —
a first-found read would let a clean mcp.json shadow a live .mcp.json —
and each record names the file it came from; auto-fix writes back to the
file it read, and the backup manifest pre-seeds both spellings. A tree with
neither spelling records not applicable naming the set, mcp.json or .mcp.json (#637).

Those checks' records also now carry file:, with two score effects, both
in the direction of counting what was measured. On a tree whose
package.json does not type it as an MCP project, the failing records were
dropped as pathless noise: a benchmark run can now report controls 2.3, 4.1
and 5.2 as measured (and failing on real configuration gaps) where it
previously reported them unverified — or passed, when the only record
that survived the drop was a passing one — while the plain secure score is
unchanged there (the project-type scope still keeps MCP checks out of the
scored list; measured unchanged on every corpus fixture and five workspace
repositories). On a tree that IS MCP-typed, the same records previously
failed the scored list's file requirement, so a failing root config now
lowers the plain score — and can lower the verdict — where it silently did
not (measured 55 to 36 on a config failing six of the checks). (#637)

The benchmark evaluator now folds every record a checkId carries instead of
keeping the last one: any failing record fails the control whatever a later
record says, a measured record outranks a not-applicable sibling, the
verdict is independent of scanner emission order, and a control cites one
evidence line per failing record — the one-result-per-failing-record shape
SARIF uses. On every corpus fixture and test tree measured, no control status,
count or compliance figure moves from the fold alone; evidence arrays grow
to cite every failing site.

Known issues

  • init-mcp for Claude Code, selected with -t claude or detected from a
    .claude/ directory, prints Detected: Claude Code and Added HackMyAgent MCP server to .claude/settings.json, and that file is the only one it
    writes. claude mcp list (Claude Code 2.1.273) in that directory prints
    No MCP servers configured, while the same mcpServers object copied into
    <dir>/.mcp.json is listed, pending approval; that copy is the measured
    workaround. Present in 0.33.0. Target 0.34.0
    (#757).
  • harden-soul on a SOUL.md holding a title, one descriptive sentence, a
    one-line Trust Hierarchy and a one-line Injection Hardening section reports
    9 sections added and writes a <!-- soul:profile=conversational -->
    marker the starting file did not have; the analyzers then disagree on the
    file it wrote. scan-soul reads the added sections as autonomous and
    reports HIGH SOUL-PROFILE-MISMATCH, 74/100 with 4 of 9 domains evaluated,
    and exits 0; check <dir> --nanomind reports HIGH No Override Resistance,
    69/100, and exits 1; secure <dir> reports 93/100, Usable with caveats,
    and exits 0, noting that governance, scope, prompt and code analysis did
    not run. Removing the profile marker, the fix scan-soul prints, takes
    scan-soul to 100/100 HARDENED and leaves check --nanomind at 69/100,
    exit 1. Present in 0.33.0; the marker half is listed under the 0.32.0 Known
    issues. Target 0.34.0
    (#446).