Skip to content

v0.63.0

Choose a tag to compare

@fdaviddpt fdaviddpt released this 19 Sep 14:53
· 3 commits to release since this release

Added

  • ops:grep=PATTERN — filter the op roster by name, syntax or description instead of piping it through grep (#1318). supertool 'ops' prints every op's signature, and finding the one op that does X meant reading all of it or piping it through grep — the exact motion the shipped raw-command guard (supertool-no-cut.md) blocks, correctly, leaving a redirect-to-a-temp-file workaround as the only route left. Three independent agents hit it in one week. ops:grep=PATTERN matches op name, syntax and description — including a dispatchable op with no .supertool.json entry, so ops:grep=introduction finds it rather than reporting 0 of M for an op that plainly exists — and prints only the matching rows, always stating N of M ops matched — 0 of M included — so an empty result can never be misread as a short roster. Case-sensitive, like every other pattern slot in this file. A pattern containing : (the shape a search for read:PATH-style syntax naturally has) reaches the filter whole rather than being silently cut at the first colon by the CLI tokenizer; an empty config falls back to a bare name list rather than "0 of 0". Reuses _pattern_gate, the single chokepoint every other pattern-taking op goes through (#2574 did the same for between), with check_saturation=False since the count line already discloses when a pattern matches everything. Scoped to bare ops, the same way ops:roster/ops:full/ops:session are — ops-compact:grep=X still refuses, naming the new token.

  • Added oss-tick, a new shipped preset (presets/oss). It composes the mechanical opening of the claude-oss maintainer tick -- last state entry, pending wait, plugin identity vs last recorded, git fetch && git pull --ff-only, the board (gh-prs/gh-issues/gh-branch/git-worktrees) and radar tier resolution -- into one round-trip receipt whose every row is one of three states, never two. It is a thin shim: it locates the installed oss plugin from Claude Code's own install record and execs that plugin's own oss_state.py, so no maintainer-loop judgment lives in this repository. (#1985)

  • youtube_reply, youtube_like, youtube_status_since — the rest of #227's YouTube write ops (#2593). youtube_reply:COMMENT_ID|TEXT_OR_file://PATH[|force][|force-dup] posts a reply via comments.insert and reads it back the same three-verdict way (verified/MISMATCH/could-not-verify) youtube_comment does, keyed for its duplicate guard on the parent comment id rather than the video so a second reply to a different comment on the same video is not wrongly refused. youtube_like:VIDEO_ID_OR_URL[|force][|force-dup] sets this account's rating to "like" via videos.rate and confirms it with videos.getRating, sharing youtube_comment's plain VIDEO_ID duplicate key on purpose (a conservative default, not a claim the two actions are the same). youtube_status_since[:ISO] lists new top-level comments on this account's own videos since a cutoff (default 24 hours), needing the same OAuth2 grant as the write ops because "own videos" is only answerable for the authorised account, but never writing, so it carries no sentinel, no rate cap and no confirmation gate. All three reuse _oauth, _yt.authorized, _sentinel and _publish_safety unchanged, per #227's own note that they were "argv and an endpoint each" once the write machinery was proved out by youtube_comment (#2543).

Fixed

  • gl-pipeline:ID:full and mcp_stop:--all are now declared where callers read them (#1315). Both tokens already worked -- gl-pipeline's _FILTERS accepted full and mcp_stop's underlying script treated --all identically to the dedicated mcp_stop_all op -- but neither syntax string nor description named them, so a caller who guessed either spelling got a working call the registry never told them about. gl-pipeline's syntax and refusal text now name full explicitly; mcp_stop's description now says --all reaches the same code path as mcp_stop_all, which stays the canonical spelling.

  • grep's 0 results output now names case-insensitivity when the pattern DOES match case-insensitively, so a write confirmed with a differently-capitalised grep no longer reads as a failed write (#1777).

  • read:PATH:OFFSET:LIMIT's disclosure of which form was read -- and how to get the other one -- is now also repeated at the foot of a long window, not only above the content, so it does not scroll off before the caller reaches the lines it corrects (#1777).

  • Corrected the last two stale ops/ops-compact/ops:full byte-count claims #1877 could not reach, plus five more stale _HOOK_OUTPUT_CAP_BYTES mentions self-review found in the same sweep (#1887). Two of the four sites #1877 handed back as report-for-filing had already been fixed by unrelated, later commits (_supertool.py:2575 by an op_ops() docstring rewrite, _supertool.py:17101's 74,838 figure by a different correction) before this lane reached them, and docs/contributing.md:122's figures already matched the render — except one clause in the same paragraph nobody had checked, which still said _HOOK_OUTPUT_CAP_BYTES = 7,168 (#2029 corrected the constant to 10,000 in 2026-08-27 and missed this exact sentence). The one live instance among the four named sites, tests/test_ops_roster_1231.py's module docstring, still stated the pre-#1774 pair (ops 47,254 bytes, ops-compact 9,067, cap ~7,168) as a present-tense fact; it is now a dated correction naming the actual figures (ops 4,710 bytes, ops-compact 18,510, cap 10,000). Self-review then found the identical stale 7,168/~7KB literal surviving in four more places tests/test_render_size_claims_1877.py's registry never reaches because they are .py prose rather than a graded doc claim: _supertool.py's op_ops() docstring and _ROSTER_LEGEND (the latter rendered live in every ops/ops:roster call), tests/test_description_is_not_a_changelog_1774.py, and tests/test_guard_scope_is_a_route_not_a_file_1671.py — whose _SESSION_CAP = 7168 module constant now derives from supertool._HOOK_OUTPUT_CAP_BYTES instead of copying the literal a second time. tests/test_render_size_claims_1877.py's UNCOVERED registry, which named the three #1877 sites as ungraded gaps, is now empty.

  • slack_authorization no longer prints a raw config-file key at column 0 (#2430). presets/slack/auth.py read channel_id -- a key straight out of ~/.config/supertool/slack_authorization.json -- and printed it unflattened at the start of a line in both _list_all() (the bare listing) and _one() (the per-channel header). A key carrying an embedded newline could render as two physical lines, the second indistinguishable from a genuine channel row -- the exact rendering shape _supertool.py already flattens for an "ops"/"aliases" config key via _flat_field(..., disclose_newline=True). Both sites now route channel_id through the preset-side equivalent, _untrusted.flat(channel_id, disclose_newline=True), with a new regression test pinning that a newline in the key cannot forge an extra row, paired with a positive control confirming two genuinely distinct channels still render as two separate rows.

  • _toml_delimiter_hint() stayed silent on a ''' embedded in prose that closes a value at the END of its own line ("...closes like this: '''"), rather than immediately before more text on the same line. The trailing garbage that early close produced landed on the NEXT line, past where the #1830 same-line check ever looked, so the payload raised a bare TOML parse error with no delimiter hint at all -- the shape the hint exists to explain, unexplained. _toml_delimiter_early_close() now walks past blank/comment lines to check whether what follows looks like a real next statement (#2545).

  • _lock_dir_usable's write probe no longer falls back with no lock held on a one-off, transient failure (#2563). Its own docstring named a transient Windows AV scan on the just-created probe file as a case it could hit, but the function had only one failure path -- any OSError on the probe returned False immediately, and serialize_once treated that the same as lock_dir being genuinely, permanently unusable, running fn() with no lock held. The probe filename is unique per call (uuid4), so contention can never trigger this; only a one-off hiccup or a genuinely broken directory can. It now retries the probe once, against a fresh probe name, after a brief pause before concluding lock_dir is unusable -- negligible against the multi-minute timeout_s this whole probe exists to avoid spending on a false positive.

  • The token-file mode-bit probe (_publish_safety._mode_bits_are_enforced) is now created next to the credential it is judging rather than in TMPDIR -- a probe on a different mount than the credential (exFAT/FAT/SMB, a container /tmp) could take the soft WARNING arm for a genuinely group/world-readable credential that lived on an enforcing filesystem. A credential directory that is missing or not writable falls back to the old behaviour, reported on stderr rather than silently (#2597).

  • KNOWN_RESIDUE in tests/test_markdownlint_noise_2012.py is now keyed by (file, rule_id) instead of by file alone (#2598). A per-file exemption made a second, unrelated markdownlint rule landing in an already-exempted file invisible to the residue test -- only the repo-wide FINDINGS_CEILING would ever have caught it, with 13 findings of headroom. _lint_all_tracked_md_with_rules() now keeps each finding's own rule code, and _residue_violations() checks per (file, rule) pair.

  • youtube_comment and youtube_auth no longer print raw OAuth/API error bodies or the cached scope string. presets/youtube/comment.py's two get_access_token()/insert-time except arms and presets/youtube/auth.py's authorize() error arm wrote sys.stderr.write(f"ERROR: {e}\n") straight from a Google-supplied body, and auth.py's youtube_auth:status printed the cached scope field the same unescaped way -- a newline anywhere in either would have put the remainder at column 0 of the receipt, exactly the shape #227 already fixed at one sibling site (comment.py:140's verify()). All four now go through repr(safe_short(str(e), 300)), matching that fixed site (#2599).

  • youtube_comment supports ::: as an unambiguous field separator, alongside |. parse_args's trailing-field scan for |force/|force-dup cannot tell "the operator's last pipe-delimited segment is body text" from "the operator meant the flag" once both spell the same word -- youtube_comment:VID|may the|force silently dropped "force" from the body and forged the publish confirmation, on the one guard standing between a call and a public, un-deletable comment. ::: is not a character ordinary prose would contain by accident, so opting into it (VIDEO:::body ending in the word force:::force) keeps a body's own trailing force/force-dup-looking text intact instead of guessing. Default | parsing is unchanged, since redefining it would silently reinterpret every already-shipped |force call site (#2600).

  • which_excluding_cwd()'s three copies (validators/common/spawnable.py, presets/_spawnable.py, _supertool.py) reported a genuinely installed tool as absent whenever the caller's PATH was unset (a stripped env= on a parent spawn, some cron/launchd contexts) rather than deliberately empty. shutil.which() itself falls back to os.confstr("CS_PATH")/os.defpath when PATH is unset, and only refuses on an explicitly empty PATH="" (bpo-35755); all three copies now mirror that distinction instead of collapsing both cases (#2603).

  • The two bare-spawn registers (tests/test_gate_matches_spawn_2579.py, tests/test_spawnable_register_2540.py) now catch what their own assertion text already claimed to cover (#2606). _bare_existence_check_without_dirname_guard() previously missed a bare-imported isfile(X) spelling and an existence check with no surrounding and-chain at all, and false-flagged a dirname() guard nested inside bool(...); it now reasons over the whole enclosing AND/OR expression by truth table rather than searching a single BoolOp's direct values, so dirname(X) or Path(X).exists() is caught as unguarded (an or needs only one operand true) while dirname(X) and (Y or (Z and Path(X).exists())) is correctly left alone -- and correlates the checked name against what a chokepoint call (spawnable()/argv0()/resolve_bin_cmd()) actually resolves, so an adapter's unrelated file-existence check (does Cargo.toml/go.mod exist?) is no longer a false positive. Both registers' own assertion text now states the scope it does not cover -- a bare, entirely ungated argv[0] literal is tests/test_bare_argv0_construction_2605.py's job instead.

  • The non-adapter bare-spawn register (tests/test_bare_spawn_cwd_gate_2596.py) now walks presets/, hooks/ and notifiers/ instead of a hand-listed six-file dict (#2611). The old SITES dict could not discover a seventh site by construction; walking the real directories found three previously invisible instances in _supertool.py itself -- _has_rtk(), _has_ctags() and _doctor_symlink() each cached a raw shutil.which() result and spawned it, the same current-directory-search shape #2596/#2575 fixed elsewhere -- now routed through this file's own _which_excluding_cwd(). hooks/guard-selftest.py's own instance of the same class is tracked separately as #2610 and carried in the register's ALLOWLIST rather than silently unwalked.

  • watches/radar's desktop-notification disclosure now distinguishes "will fire" from "opted in but it won't" -- previously it read ON whenever SUPERTOOL_WATCH_DESKTOP was set, even when desktop_notify itself would silently no-op because the platform has no osascript equivalent, or because osascript was absent or excluded by the cwd guard (#2612).

  • which_excluding_cwd()'s docstring (validators/common/spawnable.py, presets/_spawnable.py) overclaimed that any name with a directory component "carries none of" the cwd-shadowing risk. True only for an absolute component -- a relative one (./bin, tools/x) still resolves against cwd, which is deliberate and already config-gated (a validator's env may point at a relative binary path), not an oversight. Reworded to say so explicitly (#2613).

  • test_the_environment_switch_turns_it_off (tests/test_read_elide_unchanged_1329.py) is no longer order-dependent under xdist (#2619). It failed once on master (ubuntu-latest/3.11, popen-gw3) on the baseline elision check -- content came back on a second, still-unchanged read, before the environment switch was even touched. No sibling-test state leak could be found; instead the test's two op_read calls used real time.time(), unlike this file's other tests exercising the same "read twice, expect elision" shape, already frozen for the identical class of race in #2354. Reproduced directly: _read_elide's window guard is 0 <= now - prior[1] <= window -- deliberately conservative, so it declines to elide on a negative gap rather than guessing -- and a backward step of the wall clock as small as 50ms between the two calls (an NTP correction or VM clock skew, the same "loaded parallel CI runner" shape #2354 already names) makes the gap negative and the elision is correctly refused. Time is now frozen in this test, matching its siblings, and in two more tests in the same file found to have the identical unfrozen back-to-back-op_read shape (test_second_identical_read_is_elided_and_says_how_to_undo_it, test_full_never_elides_and_is_named_by_the_elision). A new positive control (test_a_backward_clock_step_still_correctly_declines_to_elide) pins the 0 <= half of the guard, which nothing exercised before.

  • _sanitize.py::wrap() and each preset's own read.py::render() (youtube, bluesky, devto, hashnode) now flatten an injection-scan hit -- both a raw newline and a raw carriage return -- before joining it into the printed POSSIBLE INJECTION warning line. detect() returns the matched substring itself, and one of its own patterns ((?:^|\n)\s*system\s*:) can match across an embedded newline (with an optional carriage return right after it) in untrusted text -- unflattened, that put attacker-chosen text at the start of a second line, or moved the cursor back to column 0 of the current one, indistinguishable from a line the tool wrote itself. #2593 shipped the same one-line fix at one call site (presets/youtube/status_since.py, on the still-unmerged fix/2593 branch / PR #2635); this closes the 8 remaining call sites across all 4 preset directories, including presets/hashnode/read.py, which the issue's own recon pass had not checked but carries the identical shape (#2636).

  • presets/oss/shim.py and presets/oss/tick.py now carry the exception's own reason through the three except-arms that previously discarded it: _load_record's except (OSError, ValueError): return {}, _install_roots's except OSError: return [], and tick.state_file's except OSError: return None, "{} not found". A genuine read/parse failure -- an unreadable or corrupt install record, a permission-denied cache root, a .oss.local.json that exists but cannot be read -- rendered identically to "legitimately not installed"/"legitimately not resolved"/"legitimately not found", which sent a maintainer whose plugin is correctly installed down tick.py's reinstall-it NEXT: line (#2638).

  • oss-tick's pending-wait guard (presets/oss/tick.py::compose) gated dispatch with rows["pending_wait"].startswith("holds"), matched against the installed oss plugin's own oss_state.py --pending-wait output -- but that script prints a holding or could-not-evaluate wait as a JSON object (json.dumps(record, indent=2)), which starts with {, never with the literal word holds. A real holding wait therefore fell through to "proceed to dispatch" instead of the do-not-dispatch NEXT: line. presets/oss/tick.py now parses the script's real shapes via two new helpers, _parse_pending_wait and _parse_plugin_identity_check -- the latter for a sibling instance of the identical root cause in the plugin identity vs last recorded row, where the script's one-line stderr receipt is merged ahead of its own JSON record on stdout by this shim's _run, and used to render raw JSON into that row's line rather than the human-readable sentence the row's own value is supposed to carry. docs/presets/oss.md's vocabulary table gains the route-mismatch state this fix now surfaces, and tests/test_oss_preset_1985.py's fixtures, which faked bare-word output the real script never produces, are corrected to its actual shapes (#2639).

Security

  • argv0()/_spawnable() no longer hand a refused cwd-only match to the OS's own search (#2578). Both chokepoints ended which_excluding_cwd(name) or name: when a match existed only via the current directory, which_excluding_cwd() correctly refused it, but the fallback then handed subprocess the bare name -- and on Windows, CreateProcess performs its own PATH+cwd search, resolving the very match this module exists to refuse. A genuinely absent tool (no match anywhere, not even cwd) is unaffected and still returns the bare name unchanged. Also routes hooks/guard-selftest.py's bash resolution through the same chokepoint (#2610), closing the same class in a pre-existing, out-of-band diagnostic that predates the audited range.