This release includes 2 security fixes for security teams reviewing exposed deployments.
Topics
+12 more
Affected surfaces
Summary
AI summaryUpdates high, P1, and repo_root across a mixed release.
Full changelog
Second whole-plugin real-invocation QA round against the released v3.1.2,
weighted toward Python/Ruby framework depth, less-covered skills, and this
project's own newest code paths. 30 findings confirmed via independent
adversarial re-verification, resulting in 25 code and doc fixes across 15
files (several confirmed findings shared one root cause). As with the prior
round, the fix pass itself was independently re-verified per finding; that
pass caught two fix-introduced regressions (both in conventions.py, both
repaired with real two-scenario regression tests — see "Fixed").
Fixed
- A same-turn fix from the prior round crashed the SessionStart/conventions.md
render entirely on a real Ruby repo (high). The casing-convention rendering
added in v3.1.2 raisedUnboundLocalErrorwhenever a method/class/constant
casing convention was "strong but not enforced" (60-95% consistency) — the
exact tier that feature exists for — silently dropping the WHOLE conventions
block, not just the casing line, and deleting a pre-existing
.chameleon/conventions.mdmirror outright.conventions.py. - Trust bypass:
explain_editandget_review_historyhad zero trust gating
(P1). Either tool replayed one checkout's archetype classification,
match_quality, blockable-rule list, or PR-review verdict history — including
free-text findings, commit SHAs, and reviewer usernames — to a caller whose
own checkout was never granted trust for that repo_id. Both now gate on
trust_state_for(repo_id).grants_root(repo_root)before serving content,
mirroring every other model-callable read tool.tools.py. get_canonical_excerpt/get_rulescould silently serve a DIFFERENT
physical clone's content when a repo_id collided across two local
checkouts of the same remote, with no way to detect the mismatch. The
resolvedrepo_rootnow rides in the envelope.tools.py.- PR-review auto-pass's
override_ratewas computed from shadow-mode
would-blocks only, never real enforce-mode blocks — an actively-enforcing,
well-behaved rule could read as a 100% override rate and get flagged
high_override_ratewith zero real sample evidence.review_ledger.py. get_callers/get_callees/get_blast_radiusmerged distinct same-named
methods from different classes in one file into a single answer,
contradicting the tools' own "fails open on any ambiguity" contract;
get_blast_radiuscould compose a fabricated multi-hop chain from two
unrelated methods that happened to share a name.calls_index.py.- The multi-lens VERIFY/refuter stage refuted correct, well-evidenced
intent-vs-code mismatch findings because it was never given the captured
intent_tokensthe correctness judge itself used to raise them — only a
source-only excerpt, which its own prompt instructs it to refute on when it
"cannot tell." The tokens are now folded into the refuter's copy of the
claim for that call only, then restored before persistence/render.
hook_helper.py. - A real block or inline override of
import-preference-violationwas
invisible to/chameleon-explain's post-incident replay — the only
PreToolUse deny branch that never called_record_edit_decision, unlike its
secret-detected-in-content and eval-call siblings.hook_helper.py. - Monorepo coordinator-only bootstraps had nowhere to persist a resolved
production_reflock, sodetect_reposilently reportedlocked: false
for a workspace whose profile really was derived from a locked branch, and
refresh_repo's inheritance fallback could silently overwrite an explicit
lock with a freshly re-detected branch. The coordinator's resolution is now
persisted off the trust-hashed surface and read by both paths.tools.py. - A no-git-remote repo that lost its
repo_uuid(deletion, bad merge, a
restored old backup) silently shiftedrepo_idto the path-hash branch,
orphaning the prior trust/drift/review-history grant with no diagnostic.
detect_reponow surfaces alegacy_trust_hint, mirroring the existing
engine-hash-change case — but declines whenconfig.jsoncarries a
persistedproduction_ref(only ever auto-stamped for an origin-backed
repo), since that combination means the repo more likely lost its git
remote, not arepo_uuidit never needed; misattributing that case pointed
at the wrong remediation (a caught-and-fixed regression in this same fix).
tools.py. refresh_repo's default (force=False) path didn't detect a
conventions.jsonwhose derived section (e.g. naming) was stripped while
the surrounding JSON stayed valid — reported noop and left the corruption in
place indefinitely.tools.py.- Pausing chameleon only silenced the auto-injected PreToolUse hook path —
a model callingget_pattern_contextdirectly during an active pause still
got full guidance. The pause state is now checked inside the tool itself,
blanking canonical/idiom/rule content with apausedreason like the
untrusted gate does. Pausing was also invisible everywhere a user could
check it (statusline,/chameleon-status,/chameleon-doctor) — the
statusline now surfaces the active window.tools.py,chameleon-statusline.sh. doctor's hook-error log scan silently discarded the exact warning that
signals a prose artifact (idioms.md/principles.md/conventions.md) was
dropped for prompt injection — a live, correctly-blocked poisoning event
left no trace in the one diagnostic tool meant to surface it.tools.py.get_status'senforcement.overridesblock was documented as
conditionally present but the code always included it, even with zero
override history ever.tools.py.- The symbol-index stale-vs-corrupt classifier treated ANY schema_version
mismatch as "stale", including a schema_version-absent garbage payload
bearing no resemblance to a real prior-schema artifact — now requires actual
shape evidence before calling it stale rather than corrupt.tools.py. search_codebasesilently reported zeroed results with no degraded flag
whencalls_index.json/symbol_signatures.jsonwere entirely MISSING,
even though the identical present-but-corrupt case correctly flagged
degraded. Both now surface a reason.tools.py._classify_casing's four casing buckets left a gap: a file stem mixing
an underscore separator with an embedded uppercase letter (test_BadCasing)
matched none of kebab/snake/camel/Pascal and returned no signal, so the
file-naming-violation rule silently never fired on exactly the shape that
most obviously breaks a repo's casing convention. Added amixed_case
bucket that counts against consistency — and, caught by this same round's
independent review, a follow-up fix somixed_casecan never itself win the
dominance vote and become the archetype's own declared "convention" (which
would have flagged every genuinely-conforming sibling as a violator).
conventions.py.- The Python naming-convention lint check was ASCII-only, misclassifying
every legal PEP-3131 unicode identifier as a casing violation even when
genuinely conforming in its own script — on aBLOCK_ELIGIBLErule, capable
of hard-blocking a correct edit. Casing classification is now based on
actual letter case (str.isupper()/isupper()semantics), not an
ASCII character-range regex.conventions.py. - A multi-base Python class's
extendsdisplay marker ("Base (+1 more)")
was reused as a dominance-grouping key, fragmenting a shared primary base
across single- and multi-base classes into separate Counter buckets and
potentially defeating a real, unanimous base-class contract — caught by this
round's independent review of the libcst_dump.py fix that introduced the
marker. Dominance grouping now uses the plain first base; the display marker
is TS/Python-render-only.conventions.py. - The journey harness's "no concurrent runner" lock could never actually
detect a concurrent invocation —build_context()created the exclusive,
uniquely-timestampedrun_dirone call before the lock was ever acquired, so
two overlapping invocations just created two different run_dirs and never
contended; a real race crashed with an unhandledFileExistsErrorinstead of
the documented cleanPreflightError. The lock now scopes to (and is
acquired against) the shared results root BEFORE any run_dir is created.
preflight.py,runner.py. - The new slash-path intent-capture regex required a leading
/, silently
truncating the far more common relative repo-path form a user actually types
(src/config/settings.ts) instead of capturing it whole. The new
single-digit numeral rules didn't exclude a following/, so a fraction
or ratio (retries=1/3) captured its numerator as a standalone checkable
constant.intent_capture.py. chameleon-teach/SKILL.mddocumented zero working examples of the
Source:provenance line for either capture mode, despite the doc's own
cap description, validation-error table, and Honesty Rules section assuming
it was in use.chameleon-deep-work/SKILL.md's dependency-baseline step
assumed a lockfile always exists, with no guidance for a lockfile-less
repo. Both docs updated.tests/bench_hot_path.py's target-file selection ignored the profiled
repo's own declared language, always preferring.tsx/.tsover.rb,
so a Ruby repo with any frontend TypeScript file silently benched an
unmatched-archetype degraded path instead of the intended Ruby hot path.
Full unit suite green (5634 passed, 3 skipped), ruff clean.
Security Fixes
- Trust bypass: `explain_edit` and `get_review_history` now gate on `trust_state_for(repo_id).grants_root(repo_root)` before serving content (P1)
- SessionStart/conventions.md rendering crash fixed in high module, preventing whole block deletion when casing convention is 'strong but not enforced'
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About Chameleon
All releases →Beta — feedback welcome: [email protected]