All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
gl-mrs— MR triage board. Lists MRs (defaultauthor=@me), sorted failing-first then stalest, each enriched in parallel with everything you'd otherwise round-trip for: pipeline status (a failure shows the failed job name —phpstan2,test_unit_dpt— i.e. the failure class, pre-empting agl-jobcall), approval state, age, diff size, watch-state cross-reference (which MRs already have a livewatchpoller), andconflict/draft/threadsflags. Footer points at the first failing-and-unwatched MR. Filters:author/reviewer/assignee/label/milestone/state/per. Flags:nopipe,iids(bare id list),failed(only failing). Tunable viaenrich_workers/enrich_cap/per_page. "Mine" is just the default filter, not a special op — enumeration is a platform concern, kept out of the genericwatchpreset. See docs/presets/gitlab.md.presets/watch/watch-mine.sh— watch-a-whole-query supervisor. Glue between a "list mine" op (gl-mrs/gh-prs) and thewatchpreset: runs the feed op, extracts ids, spawns one watcher each. Idempotent, so it's safe on/loop.gl-mrs:failed,iids | watchturns "ping me when any of my MRs goes red" into one looped command. See docs/presets/watch.md.watchpreset — background event pollers with async wake into Claude Code. New opswatch:SOURCE:ID[:only=...],unwatch:SOURCE:ID,watches. Each invocation forks a detached poller that emits state-change events to a UDS socket, a status file, and macOS Notification Center. Source-plugin contract makes new sources ~50 lines. Bundled sources:github-pr(PR state, checks, reviews, comments, merge/close, conflicts) andgitlab-mr(MR state, pipeline, merge/close, conflicts). Closes #165. See docs/presets/watch.md.notifiers/claude-channel/— MCP channel server for async wake. TypeScript / Bun server that binds the watch UDS socket and pushes events into a running Claude Code session via the Channels feature (research preview, requires Claude Code v2.1.80+). Events arrive as<channel source="claude-channel" watcher_source="<source>" id="<id>" event="<event>" ...>tags. UDS file mode 0600 + localhost-only auth model. Launch withclaude --dangerously-load-development-channels server:claude-channel. See notifiers/claude-channel/README.md.globbrace expansion.glob:src/**/*.{json,xml}now fans out into*.json+*.xml(shell/fd/ripgrep semantics) and dedupes results, instead of silently returning 0 files. Supports multiple groups ({a,b}.{x,y}→ 4) and nesting ({a,b{1,2}}→ 3). Patterns without braces are unchanged. Closes #161.validator_cache_ttl_hoursconfig (default24,0disables). Validator cache entries expire this long after being written. The cache key only hashes file content, so without a TTL an entry outlives changes it can't see — an updated adapter/rector.php, or a transient engine failure. Expiry is on access (stale → miss → re-run → rewrite); no cron needed.
- Validator cache no longer freezes transient engine failures.
rector-mcp's warm daemon intermittently trips rector's ownSystem error: "ClassReflection must be resolved for class X"reflection bug on test classes — warm-process-state dependent, not file dependent (a clean re-run and plainrectorCLI pass the same file). That failure was cached keyed on file content and replayed on every later run, including its frozenduration_ms; 2100 entries were poisoned this way across a test suite. Two-layer fix: (1)validators/rector-mcp/rector-mcp.pydropsSystem error:results at the source — an engine glitch is not a code finding; (2) the validator cache now excludes non-deterministic engine failures (MCP transport errors, non-zero exits,System error:messages) from being written. Real findings (PHPStan types,rector.refactor) stay cached. git-mergenow fetches before merging a local branch — no more silent stale merges. The docstring promised "fetch + merge" but there was no fetch:git-merge mastermerged the localmasterref, which is stale the moment origin moves ahead. Real-world bite: a fix landed on origin/master, but mergingmasterinto a feature branch silently used the lagging local ref and the fix "wasn't there" — the failing job kept failing. Now_fresh_merge_ref()resolves the ref's upstream, fetches it, and if the local branch is behind, redirects the merge to the upstream (e.g.origin/master) with a loud note:local master was N behind origin/master — merging origin/master (latest) instead. Offline / fetch-fail / no-upstream cases warn and fall back to the local ref (no hard failure). Remote-tracking refs likeorigin/masterare refreshed directly.
validators/pyright/— Python type-check viapyright --outputjson. Sits next topy-compile(syntax-only). Hooks intoedit/replace/replace_lines/paste/vimfor*.py. Graceful skip whenpyrightnot on PATH; pyright JSON 0-indexedrange.startis converted to supertool's 1-indexedline/col. Configure via.supertool.example.json. README validator count bumped to 18.
op_pastecontainment ordering. Previouslyop_pastecalledos.makedirs(parent)BEFORE_atomic_write's_safe_pathcheck, so a traversal path like../../tmp/evil/foowould create directories outside cwd before the write itself was rejected — leaving empty-dir pollution.op_pastenow runs_safe_pathat entry. Side benefit: closes the NUL-byte-in-path xfail (_safe_pathrejects NUL beforeos.replacecan leak aValueError).
- Hardening bundle. Smaller defence-in-depth items from the audit (closes #150):
- Gitcli flag smuggling:
git-checkout REF,git-merge REF,git-blame PATHreject refs/paths starting with-(e.g.--orphan=evil,--abort,-X theirs).git-blameuses--separator to keep a path-with-dash from being parsed as a flag. - Regex ReDoS guards on
grep: pattern length capped at 1000 chars; nested unbounded quantifiers ((a+)+,(.+)*) rejected before they touch file content. validate_staged/format_stagedusegit diff -z --diff-filter=ACMR(filenames with newlines/quotes survive) and reject symlinks — a staged symlink to/etc/hostswould otherwise be REWRITTEN by formatters.xmllintvalidator runs with--nonet --noentso libxml2 can't fetch external entities or follow URI references during validation (XXE defence-in-depth).- Validator cache HMAC at
~/.cache/supertool/.cache_key(mode 0600, 32 random bytes, generated on first use). Each cache entry wrapped with HMAC-SHA256 of its body. Attacker with write access to~/.cache/supertool/validators/cannot forge a passingok: trueentry without also reading the secret. Legacy unwrapped entries treated as miss.
- Gitcli flag smuggling:
- Social publishing safety. Three guards for the dev.to / bluesky publish + comment ops (closes #149):
file://body must resolve under.max/,drafts/,posts/, orblog/(relative to cwd). Closes the credential-exfil vector:bluesky_publish:file:///Users/.../app_passwordnow rejected before the file is read. Extend additively via$SUPERTOOL_PUBLISH_BODY_ALLOWLIST=path1:path2or"publish_body_allowlist": [...]in.supertool.json.- Confirm-default —
bluesky_publish/devto_publishrefuse to run without|force,SUPERTOOL_NO_PUBLISH_CONFIRM=1env, or"no_publish_confirm": truein.supertool.json. Blocks single-shot publish from a prompt-injected op. - Token-file mode check —
check_token_file_moderefuses to load tokens at mode 0644+ (group/world-readable). Mirrorsssh's SSH-key check.
- MCP daemon UDS hardened. Socket + pidfile + log moved from
/tmp/supertool-mcp-<hash>.{sock,pid,log,stderr}(world-writable parent dir, predictable hash) to per-user runtime dir ($XDG_RUNTIME_DIR/supertool/mcp/on Linux,~/Library/Caches/supertool/mcp/on macOS,~/.cache/supertool/mcp/fallback). Dir created mode0700, ownership-checked at startup. Pidfile + log files open withO_NOFOLLOW|O_CREAT|O_EXCLso symlink-squatting attacks no longer overwrite victim files. Peer-uid check on every accept (defence in depth on top of parent-dir perms). Server names validated against[A-Za-z0-9_-]{1,64}.os.umask(0o077)reset in detach. Override via$SUPERTOOL_RUNTIME_DIR. Closes #148. - Vim shell verbs gated.
:!cmd,:%!cmd,:N,M!cmd,:r !cmdare disabled by default. A prompt-injected vim payload like:!rm -rf ~is rejected with a clean error. Editor verbs (i/a/o/d/s/etc.) are unaffected. Opt-in viaSUPERTOOL_ALLOW_VIM_SHELL=1(env) or"allow_vim_shell": truein.supertool.json. Closes #147. :r FILEhonours cwd containment. Vim's:r FILE(without!) now routes through_safe_path— reading/etc/passwdor~/.ssh/id_rsainto the buffer is rejected. Opt-out is the sameSUPERTOOL_ALLOW_OUTSIDE_CWD=1as the rest of #146.- Path containment (
_safe_path). Every op path arg now resolves under cwd. A malicious.supertool.jsonor prompt-injected op likepaste:~/.ssh/authorized_keys:::pwnedorread:/etc/passwdis rejected with a clean error. Symlinks crossing the boundary are caught (realpath follows them). NUL bytes rejected early. Closes #146. - Opt-out: set
SUPERTOOL_ALLOW_OUTSIDE_CWD=1(env, one-off) or"allow_outside_cwd": truein.supertool.json(project-pinned, no env var dance). Env takes precedence. - Default excludes extended:
.env,.env.*,.max/,.ssh/,.aws/,.gnupg/,.kube/,.docker/,.terraform/,.chef/,.npm/,secrets/,credentials/are now pruned bygrep/glob/tree/mapso tokens don't surface into an LLM's context. - Windows-compat:
os.path.normcasehandles case-insensitive drive letters (c:\Users==C:\users) and separator normalization.
- Per-validator
cache: falseopt-out (_validator_run_one). Useful when the validator's input file isn't the only thing affecting results (e.g. phpunit: source + test + bootstrap + DI graph all matter, cache key only hashes the resolved file → stale hits when source changes).
validators/rector-mcp/: drops bare "would refactor X" entries that have noapplied_rectorsor diff. Rector returns these in--debugmode (which we need for speed on large configs) but they carry no actionable info. Real errors with rule names + diffs still pass through.
validators/rector-mcp/— bridges supertool validators todpt/mcp-rector-warmvia UDS daemon. ~14× faster per edit on Rector validation.validators/phpunit-mcp/— bridges todpt/mcp-phpunit-warm. ~25× faster per edit.validators/phpstan-mcp/— bridges todpt/mcp-phpstan-warm(PHPStan worker over TCP NDJSON). ~30× faster per edit.docs/mcp-warm-process-servers.md— overview of the three warm MCP servers and how to wire them.
All three adapters share the same shape: auto-spawn UDS daemon via presets/mcp/daemon.py, send MCP initialize + tools/call, format response as SCHEMA.md validator JSON.
Initial public changelog. See git history for prior versions.