feat(ruff-format): add Ruff format/lint-on-edit plugin#23
Conversation
Fourth migrated plugin: formats and lints Python (.py/.pyi) on Write|Edit via the consuming repo's own Ruff config. Follows the biome-format model: - Opt-in gate on a governing Ruff config (.ruff.toml, ruff.toml, or pyproject.toml with [tool.ruff]) — a pyproject.toml without [tool.ruff] does not opt in, mirroring Ruff's own discovery. No config -> untouched. - ruff check --fix (safe fixes only) + ruff format, then a --no-fix verify pass whose concise diagnostics surface as advisory additionalContext. - --unfixable F401 protects just-added imports from mid-edit auto-deletion (extends, not replaces, the consumer's own unfixable list — verified against ruff 0.15). - Version-aware syntax errors come from Ruff's own parser (target-version / requires-python floor), replacing the source repo's separate AST-check layer. - --force-exclude honors consumer excludes for explicitly-passed paths; --no-cache keeps .ruff_cache out of consumer repos. - Binary resolved from the repo's .venv (bin/ or Scripts/) or PATH; never downloaded. Advisory: always exits 0. Kill switch HOOK_RUFF_FORMAT_ENABLED; telemetry envelope contract v1.0 unchanged. Verified: 43/43 hook contract tests + 37/37 lib tests against ruff 0.15.20 (Windows Git Bash); claude plugin validate passes for the plugin and the catalog (--strict). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NNwc8U9MbgWsd5sbrfpPh
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NNwc8U9MbgWsd5sbrfpPh
|
Claude finished @kyle-sexton's task in 3m 12s —— View job PR Review — feat(ruff-format): add Ruff format/lint-on-edit plugin
SummaryStrong addition that follows the existing formatter-plugin pattern faithfully. The opt-in gate, advisory-exit-0 contract, Findings1. Stale guard variable in
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: faad5b17b4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Already fixed — |
The membership check in hook::read_file_path compared lexically normalized paths, so an in-project symlink pointing outside CLAUDE_PROJECT_DIR passed the prefix check while Ruff's writes landed outside the workspace (Cursor security review on PR #23, MEDIUM). Canonicalize both sides of the comparison via realpath/readlink -f before normalizing: escaping file and directory symlinks are now rejected, and a project root reached through a symlink (e.g. macOS /tmp) no longer causes false rejections. Hosts with no resolver fall back to the historical lexical comparison rather than silently disabling the hook. Adds six guard tests (lexical accept/reject plus four symlink cases, SKIP-passing where the host cannot create symlinks). Both scripts also pick up this repo's canonical shfmt style (.editorconfig: 2-space, no switch-case indent); the copies in the other plugins are synced in a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLwWqDP9dvakPSnnqRxwW4
|
Claude finished @kyle-sexton's task in 2m 34s —— View job PR Review — feat(ruff-format): add Ruff format/lint-on-edit plugin
What changed since the last reviewThree commits total:
File modes confirmed via Status of prior findings
New commit review:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 16ec6b0334
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A consumer config may set unsafe-fixes = true, aimed at interactive runs; without the explicit flag the hook's fix pass inherited it and applied rewrites Ruff labels as possibly not intent-preserving (e.g. E712 x == True -> x) on every edit, violating the documented safe-fixes-only contract (Cursor review on PR #23, P2). Same principle as --no-fix on the verify pass: edit-time behavior is pinned by flag, not left to config the consumer aimed at another workflow. Adds a contract test: with unsafe-fixes = true, the E712 comparison survives the hook and surfaces as an advisory finding instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YLwWqDP9dvakPSnnqRxwW4
|
Claude finished @kyle-sexton's task in 1m 16s —— View job PR Review — feat(ruff-format): add Ruff format/lint-on-edit plugin
What changed in the latest commit (
|
| # | Finding | Status |
|---|---|---|
| 1 | Stale guard var _MDFMT_HOOK_UTILS_LOADED in hook-utils.sh L8–9 |
Still open |
| 2 | Stale comment "markdown-formatter plugin" in hook-utils.sh L2 |
Still open |
| 3 | RC=1 + empty OUTPUT falls into "tool break" branch (minor) | Still open (acknowledged) |
| Codex | executable bit on ruff-format.sh |
Fixed in c27ccc1 ✓ |
| Cursor | Symlink escape in hook::read_file_path |
Fixed in 16ec6b0 ✓ |
| Codex P2 | --no-unsafe-fixes missing from fix pass |
Fixed in 40379b2 ✓ |
Still outstanding: stale guard variable (findings #1/#2)
File: plugins/ruff-format/hooks/hook-utils.sh L2, L8–9
# Minimal hook utility library for the markdown-formatter plugin. ← L2
...
[[ -n "${_MDFMT_HOOK_UTILS_LOADED:-}" ]] && return 0 ← L8
readonly _MDFMT_HOOK_UTILS_LOADED=1 ← L9These copy-paste artefacts from the markdown-formatter are the sole remaining blocker. As noted in the prior review: with each hook running in its own subprocess the stale name causes no collision today, but it becomes a functional bug if two plugins' hook-utils.sh files are ever sourced in the same process — the second sees the guard already set and returns without loading its own functions. Each copy needs a distinct guard until the shared-lib extraction lands.
Summary
The 40379b2 commit correctly addresses the Codex P2 finding — --no-unsafe-fixes is in the right position, the comment explains the non-obvious reason for it, and the implementation is solid. Three of the four prior findings are now resolved. The one remaining pre-merge item is still the stale guard variable and header comment in hook-utils.sh (L2, L8–9): rename _MDFMT_HOOK_UTILS_LOADED → _RUFF_FMT_HOOK_UTILS_LOADED and update the L2 comment to say "ruff-format plugin". Everything else is in good shape.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 40379b2dda
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Ruff's parser flags syntax not yet valid on the configured Python floor) | ||
| # arrive on the same channel. Exit 0 = clean, 1 = findings, 2 = Ruff itself | ||
| # failed (bad config, internal error). | ||
| OUTPUT=$(cd "$RUN_DIR" && "$RUFF_BIN" check --no-fix --output-format concise "${RUFF_COMMON[@]}" "$RUFF_ARG" 2>&1) |
There was a problem hiding this comment.
Ignore redirected Ruff output for findings
When the consuming environment sets RUFF_OUTPUT_FILE, this reporter no longer receives diagnostics on stdout/stderr: ruff check --help documents --output-file as also controlled by [env: RUFF_OUTPUT_FILE=]. In that case Ruff exits 1 with findings but OUTPUT is empty, so the hook mislabels normal lint findings as a tool break and returns no additionalContext findings; unset that env var for the hook or force the reporter output back to stdout.
Useful? React with 👍 / 👎.
## Summary Four plugins carry byte-identical `hooks/hook-utils.sh` copies — the playbook's Rule-of-Three trigger. Per the decision reached with fresh-docs research (plugins-reference + plugin-dependencies, fetched 2026-07-03), the shared lib becomes an **authoring-time SSOT with plain runtime copies**: - `lib/hook-utils.sh` is the canonical source; `scripts/sync-hook-utils.sh` propagates it into every carrying plugin (`sync` / `--check` / `--check-bump <ref>` modes). - New `hook-utils-sync` CI lane (wired into `ci-status`): fails on copy drift, and on PRs fails when the lib changed without a version bump in every carrying plugin — the plugin `version` is the update cache key, so an unbumped plugin never delivers the change. - Runtime is untouched: each installed plugin stays self-contained under cache isolation; no cross-plugin coupling; one-plugin install UX unchanged. - Rejected/deferred alternatives are recorded in the playbook with citations and revisit triggers: a dependency plugin is **not viable** (no documented mechanism exposes a dependency's install path to another plugin's hook; cache dirs are version- and SHA-suffixed by design), and marketplace-internal symlinks are **deferred** (skipped for `--plugin-dir`/local installs; Windows-fragile). All four plugin versions bumped (patch): the shared header no longer claims markdown-formatter heritage and the sourcing guard drops the `_MDFMT` prefix. **Stacked on #23** (`feat/ruff-format-plugin`) — do not merge into #23; retarget to `main` after #23 merges. ## Verification - `scripts/sync-hook-utils.sh --check` passes; all copies byte-identical to source (sha1 `e207414`). - Bump gate exercised in all three paths: lib-unchanged → pass; changed+bumped → pass; changed+stale → exit 1 naming the manifest. - All four `hook-utils.test.sh` suites pass (37/37 ×3, 43/43). - `claude plugin validate` ×4 and `--strict` catalog validation pass; shellcheck, markdownlint, actionlint, editorconfig-checker clean locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012NNwc8U9MbgWsd5sbrfpPh --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary Restores the eval/test-coverage attrition identified by the salvage sweep (tier-3: items #13, #15–#23 plus the two flagged cosmetic-severity items), recreated genericized and validated against `plugins/skill-quality/reference/evals.schema.json`. Per-plugin version bumps + CHANGELOG entries. | Plugin | Version | Restored coverage | |---|---|---| | songwriting | 0.4.0 | All 13 medley behavioral evals mapped onto the multi-skill split (workflow 3, diagnosis 3, rhyme 2, song-form 2, co-write 2, object-writing 1); zero drops | | ai-briefing | 0.4.0 | 6 engine evals + 3 synthetic fixtures via the audience-defaults seam; the legacy Grok-preload case re-derived as an unreachable-RSS visible-degrade scenario (the flag never shipped; CI contract bans the token) | | event-storming | 0.4.0 | Offline board-export eval (id 8) + 74-line fixture for `--discover-bcs` — disjoint from the live-Miro-required eval, reconciliation noted inline | | codebase-audit | 0.3.0 | Scope-boundary eval: decline settings/MCP/hooks claim-extraction, route to `/claude-config-audit:settings-audit` | | discovery | 0.5.0 | Research floor-scaling ("floors are not targets") + broad-topic doubled-minimums evals, vendor-neutral | | source-control | 0.2.0 | Readiness security-gate eval + genericized fixture, mixed-actor (bot-fix-now vs human-pause) eval, three worktree evals (dry-run report-only, invalid-name rejection, batched-gh status + graceful degrade) | | docs-hygiene | 0.4.0 | Two self-contained fixture-backed cases (compress classification-table, declutter opt-out/section-exemption) — fixtures empirically verified against `detect.sh`; rename-references "add an eval case" clauses | | review-toolkit | 0.6.0 | code-review-fanout evals 6 → 20: dedup/severity-derivation, fix-pass safety fence (correctness never routed to /simplify), run-everything null-reconciliation + priority-ordering; 2 medley cases skipped (fixed-roster counts deliberately generalized away), their surviving assertions folded in | ## Verification - All 8 plugins pass `claude plugin validate`; `scripts/validate-plugin-contracts.mjs` passes (1311 files) - Every evals.json jq-parses and validates against the schema (ajv/check-jsonschema) - markdownlint-cli2 0 errors on new fixtures + CHANGELOGs; coupling grep (medley/consumer refs) clean - Rebased onto main post-#192; per-plugin validate re-run green after rebase 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_011xHkkNc7CR98L8Xz9Mu7ZA --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e IV, flip pre-build (#939) Dated Phase-4 note: operator ruling (2026-07-22) to move at maximum speed within the predicate's intent. Calendar gates stay binding; sanctioned levers are front-loaded seeding (genuine items only), parallel Phase IV ignition (gate already met), and pre-built Phase III flip machinery. Records same-day watch activity: drain PRs #23/#24/#26 + Dependabot #25 merged in kyle-sexton/autonomy-demo-scratch, queue reseeded (scratch#27–#31), predicate completions=6. ## Related - kyle-sexton/autonomy-demo-scratch#27, #28, #29, #30, #31. No linked issue — status-note documentation; nothing in this repo closes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>



Summary
Fourth migrated plugin, continuing the format-on-edit family (markdown → shell → JS/TS → Python). On every
Write/Editof a.py/.pyifile it runs the consuming repo's own Ruff:check --fix(safe fixes only) +format, then surfaces residual diagnostics as advisoryadditionalContext. Always exits 0 — commit hooks / CI stay the hard gate.Design decisions (all verified empirically against ruff 0.15.20 + fetched docs)
.ruff.toml,ruff.toml, orpyproject.tomlwith[tool.ruff]), discovered by walking up from the file — mirroring Ruff's own file-anchored discovery, including ignoring apyproject.tomlwithout[tool.ruff]. No config → file untouched (never impose built-in defaults).--unfixable F401: a just-added import is reported but never auto-deleted mid-edit (the code using it often arrives on the next edit). Verified the CLI flag extends the consumer config's ownunfixablelist rather than replacing it.target-version/ therequires-pythonfloor — this replaces the source repo's separate AST-check script, so no second Python asset is bundled.--force-excludehonors consumer excludes even for the explicitly-passed path (excluded file → untouched, no advisory noise).--no-cachekeeps.ruff_cacheout of consumer repos on edits..venv(bin/orScripts/) walking up from the file, elsePATH; never downloaded on a per-edit hook.HOOK_RUFF_FORMAT_ENABLED; telemetry envelope contract v1.0 unchanged;hook-utils.shis a byte-identical copy of the lib the three shipped plugins already carry.Verification
ruff-format.test.sh: 43/43 black-box contract assertions (opt-in gate incl. both pyproject variants, format/fix, F401 preservation, syntax + target-version findings, exclude respect, kill switch, PATH fallback, telemetry envelope) in throwaway non-source repos — Windows Git Bash + ruff 0.15.20.hook-utils.test.sh: 37/37.claude plugin validate ./plugins/ruff-formatandclaude plugin validate --strict .both pass.Notes
hook-utils.shcopies, the Rule of Three for extracting a shared lib (playbook "wait on" item) is arguably met — flagged as follow-up, not done here.python-format.sh+ retiring its AST-check script) is consumer-side follow-up per the playbook, alongside the still-pending bash-lint/biome-format reintegrations.🤖 Generated with Claude Code
https://claude.ai/code/session_012NNwc8U9MbgWsd5sbrfpPh
Note
Low Risk
Adds an advisory, opt-in dev hook that mutates only opted-in Python files in the consumer repo; no auth, data, or core infrastructure changes in this marketplace repo.
Overview
Introduces
ruff-format, the fourth format-on-edit hook in the marketplace, and registers it inmarketplace.jsonand the root README catalog.On
Write/Editof Python files, aPostToolUsehook runs the consuming repo's Ruff (check --fixwith safe fixes only, thenformat), then returns residual diagnostics as advisoryadditionalContextwhile always exiting 0. Ruff runs only when a governing config exists (.ruff.toml,ruff.toml, orpyproject.tomlwith[tool.ruff]), discovered by walking up from the file; no config means the file is left unchanged. Notable hook behavior includes--unfixable F401,--force-exclude,--no-cache,.venv/PATH binary resolution (no on-the-fly install), kill switchHOOK_RUFF_FORMAT_ENABLED, and telemetry envelope v1.0 via a copiedhook-utils.sh.The PR ships plugin manifest,
hooks.json,ruff-format.sh, README, and black-box (ruff-format.test.sh) plushook-utils.test.shcontract tests.Reviewed by Cursor Bugbot for commit 40379b2. Bugbot is set up for automated code reviews on this repo. Configure here.