<feed xmlns='http://www.w3.org/2005/Atom'>
<title>rulesets/languages, branch main</title>
<subtitle>Claude Code skills, rules, and language bundles
</subtitle>
<id>https://git.cjennings.net/rulesets/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/rulesets/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/'/>
<updated>2026-07-24T17:28:18+00:00</updated>
<entry>
<title>fix(hooks): the secret scan no longer passes when git fails</title>
<updated>2026-07-24T17:28:18+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-24T17:28:18+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=f0c1bc40708615d5b423922c08c1f27e6cf96259'/>
<id>urn:sha1:f0c1bc40708615d5b423922c08c1f27e6cf96259</id>
<content type='text'>
Every bundle built its scan input as `git diff --cached ... | grep ... || true`. The `|| true` has to stay, since grep exits 1 when it matches nothing and that is the ordinary case. But with no pipefail it also swallowed a failure of git itself, so an empty result made the scan search nothing, find nothing, and report clean with a real credential staged. A gate that passes without having looked.

.emacs.d found it in elisp. It was in all five: bash, elisp, go, python and typescript, eleven sites once each bundle's staged-file list is counted. Two of those bundles are ones I wrote yesterday by copying bash, so I propagated it while closing an unrelated gap in the same file. Each site now reads the diff on its own and aborts if git fails, leaving the greps their `|| true`.

Verified per bundle on three axes: refuses when the diff cannot be read, still blocks a real staged secret, still passes a clean commit.

The cross-bundle test suite had its own version of the same disease. Its VARIANTS list read "elisp bash go" while python and typescript also shipped hooks, so every "in every variant" assertion had quietly skipped two bundles since the day they were added. VARIANTS is now discovered from the tree. Two fail-closed assertions join it, and .emacs.d's elisp suite is adopted here beside the canonical hook, because a test living in the consuming project cannot fail when the canonical regresses.

Also removes the validate-el auto-test cap, Craig's call. Above 20 matching test files the runner skipped everything and exited 0 with no output. The premise was speed and it did not hold: a whole family runs in about a second. The cap was also hiding a real cross-test pollution bug that only surfaces when a family runs in one process.
</content>
</entry>
<entry>
<title>feat(languages): ship the missing python and typescript hooks</title>
<updated>2026-07-24T01:48:44+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-24T01:48:44+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=c238afbd4150ef53737f16e7dd84a565d2838ecc'/>
<id>urn:sha1:c238afbd4150ef53737f16e7dd84a565d2838ecc</id>
<content type='text'>
The python and typescript bundles carried rules and a coverage script but no pre-commit hook, so any project installing one got no credential scan on commit. Both now ship all four components the README documents: the shared secret scan, a validate-on-edit hook, settings wiring, and a seed CLAUDE.md. Verified against a real repo: a commit carrying an AWS key is refused.

install-lang now warns when a bundle is missing a documented component. That's the half that keeps this from recurring. Whoever adds the sixth bundle will forget something too, and today the installer prints success either way.

Two things fell out of the build. node --check is unusable on TypeScript: it ignores --experimental-strip-types, so it rejects valid TS and accepts broken TS. The hook uses tsc filtered to syntactic diagnostics instead. And completing the bundles means every pair now collides on settings.json and pre-commit, so no two compose without FORCE=1. The earlier "bundles already compose" reading rested on these two being incomplete. Filed for a real decision; clock-panel is the project that wants it.

Also stamps :LAST_REVIEWED: at task creation, with a lint checker to catch misses. Writing a task is reviewing it, so leaving the stamp off pushed every fresh task to the top of the next review batch to be re-derived by someone with less context than its author had. Tonight's sweep sent the staleness count from 13 to 22 while the list got more accurate.
</content>
</entry>
<entry>
<title>docs(elisp): mark coverage-summary.el as a local-only helper</title>
<updated>2026-07-19T01:08:49+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-19T01:08:49+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=2cb7c1b3957193f74b1ecd1b5e0f437e13092eb1'/>
<id>urn:sha1:2cb7c1b3957193f74b1ecd1b5e0f437e13092eb1</id>
<content type='text'>
The elisp bundle installs coverage-summary.el into .claude/scripts/, which is gitignored in code projects, so CI can't run make coverage-summary against it. That's intentional (Craig, 2026-06-28): it stays a developer-run check, not a CI gate. Stated the local-only status in the script's commentary header and in elisp-testing.md's coverage section so the gitignored install reads as by-design rather than a gap.
</content>
</entry>
<entry>
<title>fix(githooks): match fixed-case secret tokens case-sensitively</title>
<updated>2026-07-16T16:19:45+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-16T16:19:45+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=794a8bd606006cc21351be098a30cbbfe55750e4'/>
<id>urn:sha1:794a8bd606006cc21351be098a30cbbfe55750e4</id>
<content type='text'>
The secret scan ran every pattern through grep -iE, so AKIA[0-9A-Z]{16} matched any mixed-case 20-char run. Random base64 in an embedded image blob hits that about 6% of the time per 100KB, enough to block a real commit and force --no-verify. takuzu hit it on a PNG sprite. AWS keys are uppercase, sk- keys lowercase, and PEM headers fixed, so those three now match case-sensitively. Only the keyword=value patterns still need -i.

I measured both causes first. Across ~10MB of random base64 the old pattern produced 7 matches and would have false-positived 6 of 100 sprite-sized blobs. Case-sensitive matching produced none.

takuzu's report also proposed skipping any added line containing ";base64,". I left that out. It guards against an uppercase AKIA run surviving the case fix, which never occurred in the sample and runs about one in a million per blob. The cost is real: minified bundles put a whole file on one line, so a data URI and a live key can share it, and skipping the line hides the key. A test covers that.

The three variants share one scan block, so elisp, bash, and go all carry the change. Splitting one grep into two meant a line matching both passes got reported twice, which reads as two separate leaks. awk dedupes it.
</content>
</entry>
<entry>
<title>fix(elisp): validate-el.sh prefers source over stale bytecode</title>
<updated>2026-07-14T04:39:07+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-14T04:39:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=7e61dc0fa44e9b782f2a551e3813bb11eed20af7'/>
<id>urn:sha1:7e61dc0fa44e9b782f2a551e3813bb11eed20af7</id>
<content type='text'>
Both emacs invocations in the hook now set load-prefer-newer, matching the project-side Makefile and early-init guards. Without it, the PostToolUse hook after a pull that changed a module ran tests against the module's stale .elc and every test failed void-function on the newly-added symbols. From .emacs.d's 2026-07-13 handoff, repro-verified there; inbox/2026-07-13-1712-from-.emacs.d.
</content>
</entry>
<entry>
<title>fix(elisp): cd to tests/ before the Phase 2 test load</title>
<updated>2026-06-24T03:16:03+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-24T03:16:03+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=e5aab199cd4c83f357ff5190139ddf2994ac28a3'/>
<id>urn:sha1:e5aab199cd4c83f357ff5190139ddf2994ac28a3</id>
<content type='text'>
Phase 2's plain `-l` load ran emacs from PROJECT_ROOT, so test files that resolve test-bootstrap.el with `(expand-file-name "test-bootstrap.el")` against default-directory couldn't find it and failed with "Cannot open load file". Phase 1 escaped this because byte-compile-file rebinds default-directory to the source file's dir. The cd restores the tests/ working directory the tests/Makefile already runs from.

Restores b2e9038, dropped when a consuming project's .claude refreshed to the rulesets canonical, which never carried the fix.

Claude-Session: https://claude.ai/code/session_017PtX1nt1rtYVATuzmzBS4f
</content>
</entry>
<entry>
<title>feat(languages): add bash/shell bundle</title>
<updated>2026-06-24T01:13:26+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-24T01:13:26+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=36262858461711bcb104896007a513691113fee8'/>
<id>urn:sha1:36262858461711bcb104896007a513691113fee8</id>
<content type='text'>
Shell-heavy projects had no bundle that fit. archangel and archsetup are bash repos, and installing elisp or python gave them the wrong language rules. I added languages/bash on the go bundle's shape.

The bundle ships bash.md and bash-testing.md rules, a PostToolUse hook that runs shellcheck on edited shell files and blocks on a violation, a shellcheck pre-commit githook, settings.json wiring, gitignore-add.txt, and a "Bash/shell project" CLAUDE.md. The hook covers .sh, .bash, and extensionless files with a shell shebang, since the CLI tools that fill a shell repo carry no extension. shellcheck is the gate. shfmt stays out of the blocking path because shell has no canonical formatting style, and forcing tabs-vs-spaces would impose a contested choice. Both the hook and the githook are shellcheck-clean against their own rule.

I extended the Makefile test target to discover languages/*/tests/*.bats, so the bundle's 8 hook tests run with the rest of the suite. The README bundle table was stale, listing elisp only. I corrected it to the five bundles now shipping.
</content>
</entry>
<entry>
<title>feat(install-lang): seed a neutral CLAUDE.md when a bundle ships none</title>
<updated>2026-06-24T01:00:11+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-24T01:00:11+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=71db71b9d47ffbeaf1d1c859fa3e3bebb7b2ea29'/>
<id>urn:sha1:71db71b9d47ffbeaf1d1c859fa3e3bebb7b2ea29</id>
<content type='text'>
install-lang only seeded CLAUDE.md if the chosen bundle shipped one. elisp and go do, python and typescript don't. A project installing a template-less bundle got no CLAUDE.md, and a multi-bundle install inherited whichever bundle shipped one. A bash project that installed elisp and python ended up headed "Elisp project," worse than no header.

I added a language-neutral default (languages/default-CLAUDE.md) that names no language, so single-language, multi-bundle, and wrong-bundle installs all get an accurate "fill this in" header instead of a false one. Per-bundle templates still win where present. The seed-on-first-install, no-overwrite logic is unchanged.

I hardened the Makefile LANGUAGES glob to directories only so the new template file doesn't show up as a selectable language. lint covers the default. The install-lang tests cover the fallback, the bundle-wins branch, and no-overwrite.

A bash bundle is still the real gap for shell-heavy projects, filed as a backlog task.
</content>
</entry>
<entry>
<title>test(elisp): cover coverage-summary source-file scan and under-dir filtering</title>
<updated>2026-06-22T03:59:29+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-22T03:59:29+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=0751b3c0f4e56061e79aa408493f77510e08f053'/>
<id>urn:sha1:0751b3c0f4e56061e79aa408493f77510e08f053</id>
<content type='text'>
Adds direct tests for cj/coverage-summary--source-files (non-recursive, subdirectories out of scope) and --under-dir (report entries outside the source dir dropped, survivors re-keyed relative to project root). Both were previously exercised only indirectly through the missing-file tests.
</content>
</entry>
<entry>
<title>fix(elisp): exclude generated package files from coverage source scan</title>
<updated>2026-06-22T03:57:34+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-22T03:57:34+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/rulesets/commit/?id=fb867366f4760da94f732c296601f7c324de0418'/>
<id>urn:sha1:fb867366f4760da94f732c296601f7c324de0418</id>
<content type='text'>
cj/coverage-summary--source-files scanned SOURCE-DIR for *.el and treated every match as testable source. Build tools write NAME-autoloads.el and NAME-pkg.el into that dir, and undercover never instruments them, so the missing-file detection counted each as untested at 0% and dragged the file-weighted project number down. emacs-wttrin read 72.9% instead of 97.2% after eask wrote its autoloads file during a coverage run.

seq-remove the generated files before returning. A genuinely untested source is still flagged. The new test asserts both.
</content>
</entry>
</feed>
