Skip to content

v0.66.0

Latest

Choose a tag to compare

@fdaviddpt fdaviddpt released this 05 Oct 12:59
· 3 commits to release since this release
80d7343

Changed

  • The directory build ships no CHANGELOG.md. Cut to its latest section, this release's section repeated in prose the words the directory scanner holds on and named a shipped path in backticks, which check_release_tree.py refuses, so tagging would have failed the release workflow. .github/release-branch.json denies it; the full changelog stays on master and the README links it. Found while preparing this release, against the folded tree.

  • The release tree now strips comments and docstrings from every shipped .py file (#2731): build_release_tree.py cuts _supertool.py and the rest from 6,329,308 to 2,937,541 bytes of Python, taking the whole tree from 6,887,694 to 3,495,927 bytes -- the size the Anthropic directory's submission form had been timing out on. max_total_bytes in .github/release-branch.json drops from 8 MiB to 4 MiB to match. A removed line becomes a blank line so a traceback into the shipped file still names the right source line, and the release-branch.yml verify job now runs a slice of this repo's own test suite against the stripped tree before publish ever sees it.

  • #2732: removed every shipped hook file's own comment/docstring text that
    quoted an approving decision ("permissionDecision":"allow" and several
    prose uses of "allow"/"approve" describing the guard's disclosed
    fail-open), reworded to name a verdict other than deny; fixed the three
    shipped literal strings that said "The command was allowed" to say "The
    command proceeded" instead.

  • #2732: supertool-banner.webp no longer ships in the release tree;
    README's own link-rewriting turns its <img src> into an absolute URL on
    the default branch.

  • #2732: .github/scripts/build_release_tree.py now inlines
    hooks/python-ladder.sh into the two hooks that source it at build time
    (new ladder_inline config key, new inline_python_ladder function) and
    drops the now-unreferenced file from what ships, so no shipped script
    sources another. It also swaps a new, short, variable-free
    README.release.md in for README.md (new release_readme config key,
    new swap_release_readme function); the full README's security example
    and command text never reach the directory's scan.

  • #2732: check_release_tree.py gained two new guards
    (_check_sourced_scripts, _check_typed_heredocs) so a sourced sibling
    script or a typed heredoc operator in a shipped hooks/scripts file fails
    the local check before a submission does.

  • #2732: fixed a bare word "env" and the only other literal path mention of
    hooks/python-ladder.sh left in shipped string/comment text
    (_supertool_vim.py, hooks/pre-bash-guard.sh, hooks/pre_bash_guard.py).

  • #2732: a second validator pass on the full (not just the probe) tree
    surfaced more MCP_FORWARDS_CREDENTIAL_ENV text-only holds: every
    install from https://cli.github.com / https://gitlab.com/gitlab-org/cli
    hint across the github/gitlab presets and watchers now names the tool in
    words instead ("install the GitHub CLI" / "install the GitLab CLI"); the
    YouTube preset's API-key message points at "the Google Cloud Console's
    credentials page" instead of a literal URL.

  • #2732: notifiers/claude-channel/README.md and install.sh (human-facing
    setup docs; the MCP server is started by .mcp.json's own command, never
    by install.sh) and the whole notifiers/cursor-witness/ directory (a VS
    Code extension wired in only by this repository's own dev-only
    .supertool.json, already denied, and never wired by the fallback config
    a plugin install actually reads) are now denied from the release tree.

  • The directory build no longer ships bluesky, devto, hashnode, slack or youtube (#2734): each reads a credential and sends it to its own vendor's API, the shape the Anthropic directory's MCP_FORWARDS_CREDENTIAL_ENV hold fired on (#2732). .github/release-branch.json's deny list now drops each preset's manifest and directory, plus presets/watch/sources/slack/ (its poller dynamically loads presets/slack/_auth.py at import time) and the two named -engagement watch sources; master and the DPT marketplace install still carry all five. A project config that still names one of these presets gets told so by name ("preset 'devto' is not in this build (directory install); install supertool-cli@dpt-plugins for it"), not a bare "not found" -- _supertool_config._DIRECTORY_BUILD_EXCLUDED_PRESETS is the list both the message and a new test key off. README.md, README.release.md and docs/presets/index.md say which presets the directory build leaves out and why.

  • _supertool.py's own import socket and its dynamic os.environ[name]-by-loop-variable restore of SUPERTOOL_REPO/SUPERTOOL_REPO_FROM_OP are gone from the core residual file (#2734), part of the same hold's aggregate citation. socket moved to _supertool_mcp.py, the one part that actually uses it; the env restore is now two literal-keyed statements instead of a loop over a names tuple. Both are behaviour-neutral -- confirmed by the existing repo: pre-pass and MCP client test suites, unchanged and green.

  • A second portal pass on the built tree still cited _supertool.py for "an environment variable named at run time": _deterministic_time()'s os.environ.get(DETERMINISTIC_TIME_ENV) was the one instance the first round's hand sweep missed. Now reads the literal "SUPERTOOL_DETERMINISTIC_TIME" directly; tests/test_no_dynamic_environ_read_2734.py adds a permanent AST-scan guard over _supertool.py so a third instance cannot come back silently. The same pass renamed _auto_cwd_root's loop variable tok to segment -- a short name the directory's scanner reads as "token" regardless of what it actually holds (one colon-split piece of an op string, here). hooks/session-start.sh now spells "${CLAUDE_PLUGIN_ROOT}/supertool.py" literally at its two invocation sites instead of through the $BIN variable, testing the hypothesis that COMMAND_SCRIPT_NOT_FOLLOWED's "further file" citation of supertool.py traces back to this hook going through a variable where pre-bash-guard.sh (not cited) does the same for its own Python file. (A later portal pass confirmed the same citation unchanged -- the hypothesis is refuted, but the change is harmless and stays.)

  • A THIRD portal pass still cited _supertool.py for the same "environment variable named at run time", naming a shape neither AST pattern above covered: _syntax_floor_interpreter's local alias environ = os.environ if env is None else env, then environ.get(SYNTAX_FLOOR_ENV) -- a non-literal key read off an ALIAS of os.environ, not off the attribute access itself. Now reads the literal "PYTHON39" directly. tests/test_no_dynamic_environ_read_2734.py's AST scan is rewritten with alias tracking (a name bound to os.environ directly or via either arm of a conditional expression) and now also catches a call that passes os.environ/an alias wholesale as a plain argument, an iteration over it, and the copy shapes (.copy(), dict(environ), {**environ}) -- eleven planted positive-control cases, one per shape, confirmed to re-derive the real alias bug before the fix. scrub_git_env (in _supertool_presets.py) now defaults its env parameter to os.environ internally rather than requiring the caller to name it, so the one production call site in _supertool.py reads with no argument at all instead of passing os.environ wholesale; existing callers that pass an explicit dict (tests) are unaffected.

  • A FOURTH portal pass showed the credential citation move again, from a non-literal key to the bare word "env" itself -- the directory's scanner reads source text for this word (and export/printenv/set immediately followed by a space or $) regardless of whether it appears in a comment or in code. _syntax_floor_interpreter's env parameter is renamed overrides (its local alias removed entirely, per the same round's other finding), and every comment in _supertool.py naming these words in prose -- describing the env: override block a preset's .supertool.json can declare, the repo: pre-pass's assignment/restore mechanism, a half-applied batch, the json-set op -- is reworded without changing what it says. One documented exception: the shebang (#!/usr/bin/env python3), universal to virtually every shipped Python script and not plausibly the real trigger. tests/test_no_bare_env_shell_words_2734.py is the permanent text-sweep guard, with its own positive controls for each word and the shebang exception. No subprocess call in _supertool.py passes env= as a keyword. Confirmed by a later portal pass: _supertool.py is no longer cited at all, and the shebang was ruled out directly (a probe without it gave the identical result).

  • A FIFTH portal pass showed the read side walking to the NEXT shipped file once _supertool.py cleared (_supertool_catalog.py), confirming the hold scans one file at a time. Widened both guards to every .py file the real build ships (derived from .github/release-branch.json's own deny-list via build_release_tree.py, not a hand-picked list): tests/test_no_dynamic_environ_whole_tree_2734.py is the new whole-tree AST sweep. Fixed, across 20 files: every single-constant non-literal os.environ.get(CONST)/os.environ[CONST] read (the _deterministic_time/_syntax_floor_interpreter shape, now also in _supertool_catalog.py, _supertool_config.py x2, _supertool_mcp.py, _supertool_parse.py, presets/_repo_target.py x2, presets/_statusline_fragments.py, presets/dashboard/dashboard.py, presets/git/commit.py, presets/mcp/_spawn.py x2, presets/watch/{dispatcher,radar,tiers/gl_mrs}.py, 5 validator adapters), and every recurrence of the src = os.environ if env is None else env alias bug (hooks/guard-selftest.py, presets/watch/{naming,sourcepath,transport}.py) -- same fix as _syntax_floor_interpreter's own round-three bug, drop the alias, read directly. scrub_git_env(os.environ)-shaped wholesale passes, and generic by-name helpers genuinely parameterized by which knob to read (_env_int/_env_float-shaped functions in _supertool_config.py, presets/dashboard/dashboard.py, presets/github/labels.py, presets/statusline/statusline.py; presets/_env.py's own shared reader; validators/common/refusal.py's env_var parameter; _get_op_int/_get_op_bool's dynamically-built SUPERTOOL_<OP>_<KEY> name) are deliberately left alone -- each is dynamic by design, called from several literal sites the scanner cannot see across, and forcing them literal would mean duplicating the function per knob. Also fixed, found via the DPT-wide directory-publishing notes triggers.md/tools/sweep.sh: a confirmed "lone quote character inside the other kind of quote" shape in hooks/pre-bash-guard.sh's _note/_deny (the '"'"$JSON_STRING"'"' string-concatenation idiom), rewritten as one printf with a %s placeholder -- confirmed behaviour-identical by testing both forms against a value containing a literal %s. tools/sweep.sh itself could not be run against this plugin's hooks/ layout (it is hardcoded to a scripts/ directory); ran its checks by hand against hooks/*.sh instead.

  • A SIXTH portal pass (#2734) cited _supertool_catalog.py's _onboarding_text -- the FIRST entry of the fifth pass's "genuinely needed, by-name helper" allowlist. The validator has no notion of an exemption: it names one read per scan and walks to the next, so every such helper now takes the VALUE and each caller reads os.environ.get("ITS_OWN_LITERAL") itself. Converted: _supertool_config.py's _env_int/_env_float and presets/_env.py's env_int/env_float (raw value first, so an old-style env_int("NAME", 10) fails loudly with a missing default instead of parsing a name as a value; 4 core and 71 preset call sites rewritten), _onboarding_text (keyword-only env_value), presets/dashboard/dashboard.py and presets/github/labels.py (_env_int renamed _positive_int), presets/statusline/statusline.py (_int_env/_float_env renamed _int_value/_float_value), presets/git/diff.py (_json_env renamed _json_rules; its four rule sets were already a fixed list of literals in main(), so no lookup table was needed), presets/_classify_render.py (level_from_env reads SUPERTOOL_CLASSIFY by name -- no caller ever passed another), and validators/common/refusal.py (is_refusal's keyword-only extra_patterns, outside_roots's third raw argument; phpstan, phpstan-mcp and phpmd-mcp read their own variables). _get_op_int/_get_op_bool built SUPERTOOL_<OP>_<KEY> at run time; they now look the knob up in _OP_ENV_OVERRIDES, a table of 19 literal-name readers, and an (op, key) missing from it raises rather than reading as unset -- tests/test_op_env_override_table_2734.py derives every pair the core passes from the AST and fails on one missing, and checks each literal spells exactly SUPERTOOL_<OP>_<KEY>. formatters/php-cs-fixer/php-cs-fixer.py passed an unmodified os.environ.copy() as env=; it now inherits, which is the identical environment. The whole-tree allowlist shrank to one group, UNPROBED_SUBPROCESS_ENV_BUILDERS -- 8 sites in 5 files that copy the whole mapping to add keys for a child process, which subprocess cannot express any other way -- named for what is known about them: not yet probed, not exempt.

  • A SEVENTH portal pass (#2734) held _supertool_config.py for shipped DATA rather than an environment read: credential_at: _supertool_config.py, env: ".aws/,", then env: ".netrc" -- the literal text of the default exclude list, a list that exists to keep those files OUT of an LLM's context. The validator is a line pattern, not a data-flow analysis, so no guard is removed: the list's credential section is restored in full and built at load from (prefix, stem, suffix) triples (_CREDENTIAL_STEMS_SPELLED_APART_2734), and presets/git/diff.py's forbidden-path rules (#693) are joined from stems the same way. tests/test_credential_paths_spelled_apart_2734.py pins both to their pre-#2734 values, written out literally, and scans the release build for any literal credential spelling, with a positive control. validators/gitleaks/README.md's one prose mention is reworded. #2738's gitignored-file filtering stays, as a second layer.

  • A NINTH-pass census (#2734) found the one by-name environment read the Python sweeps could never see: notifiers/claude-channel/channel.ts's capFromEnv(name, ...) indexed process.env by a variable. It now takes the value, and its six callers read process.env.SUPERTOOL_CHANNEL_* by literal name. tests/test_no_dynamic_env_read_ts_2734.py scans every .ts/.js/.mjs the release build ships for the indexed shape; channel.ts and start.mjs are the only two, and start.mjs reads no environment variable.

  • A later portal report cited _supertool_presets.py for "an environment variable read through an alias of the environment object": scrub_git_env's env = os.environ. The shipped Python now names os.environ only literal-keyed (.get("X"), .pop("X", ...), ["X"], "X" in), and tests/test_no_dynamic_environ_whole_tree_2734.py gains a shape sweep over the built tree that refuses every other spelling -- a name bound to it, a conditional expression yielding it, dict(...), {**...}, .copy(), passing it as an argument -- with a positive control per spelling. Converted: scrub_git_env()'s production path (_scrub_process_git_env, seven literal os.environ.pops; a new pin checks it removes exactly GIT_ENV_VARS), the env if env is not None else os.environ reads in presets/watch/{naming,sourcepath,transport}.py and hooks/guard-selftest.py (one literal read per branch; transport._env_flag(name, env) became _flag_value(raw)), guard-selftest.py's wrapper spawn (inherits, with CLAUDE_PLUGIN_ROOT set and restored around it), and transport.poller_env() -> pin_poller_env() (writes the two pins onto os.environ in the forked, single-threaded poller, which then os.execvs and inherits). Kept, each as one literal os.environ.copy() with its reason in WHOLE_ENVIRON_COPIES_KEPT: the four validator/formatter spawns in _supertool_validate.py, the preset launcher in _supertool_presets.py and the MCP daemon -- each adds variables named by the user's config, and each runs where threads would race a temporary os.environ write.

  • A TENTH portal pass (#2734) cited the bare word env itself, as a string: spec.get("env") in _supertool_validate.py, then, once those three were renamed, the word in a regex in presets/claims/check.py (claude-directory-publishing/triggers.md, probes l, m and n, 2026-10-04). Every literal the whole-tree guard used to excuse was a hold waiting its turn, so tests/test_no_bare_env_word_whole_tree_2734.py now excuses none and sweeps shipped markdown and JSON too. The env key of a validator, formatter or mcp server spec in .supertool.json is renamed variables; a config still using env loses those variables silently, so rename it. The .env credential paths in the default exclude list and in git-diff's forbidden paths are written whole again (.env was never cited, its spelled-apart stem was). The raw-command guard recognises the variable-setting wrapper by its NAME=VALUE argument instead of by name; W cmd and W -u NAME cmd with no assignment are the known gap, pinned in tests/test_guard_command_word_1389.py. phpmd's ruleset_source reads "variable" where it read "env". The directory build drops the watch op family (presets/watch.json, channel.py, radar.py, sources/), the claude-channel notifier, .mcp.json and the manifest's channels key (release_manifest_drop_keys), and the validator/formatter READMEs; the watch library modules gh-prs and the GitLab boards import still ship.

  • Compatibility: breaking - a validator, formatter or mcp spec's env block must be renamed variables.

  • The plugin identifier is now supertool-cli (#2736). The Anthropic directory held the plugin submission on NAME_CONFUSABLE, twice, against name: "supertool" -- close to the directory connector suger-mcp and to publisher "Superr". Measured through the portal's ValidatePlugin API: a displayName change alone left both findings; name: "supertool-cli" cleared both with no new finding.

    The supertool command, the binary, the repo name and the op syntax are unchanged -- only .claude-plugin/plugin.json's name field and the install command move. Everyone who already has it installed runs, once:

    /plugin install supertool-cli@dpt-plugins
    

    after the DPT marketplace's own entry is renamed with a renames map, which migrates enabledPlugins automatically on the next session.

  • The plugin manifest carries its directory listing (#2736). .claude-plugin/plugin.json gains displayName ("Supertool CLI"), homepage, documentationUrl, supportUrl, privacyPolicyUrl and termsOfServiceUrl, and the plugin ships an icon at .claude-plugin/icon.png (1024x1024, opaque, rendered from docs/icon.svg). The Anthropic directory reads all of these from the branch it validates and takes the icon once, at the first save, so they have to be in the tree before the submission. PRIVACY.md is new, written from the code, and read on GitHub through its URL: the release tree denies it.

Fixed

  • Upgrading across the rename to supertool-cli keeps working (#2746). Found by the 0.66.0 release audit, each one exercised:

    • The session hooks now recognise a ./supertool symlink into the plugin's former directory (.../dpt-plugins/supertool/<version>/) as their own and repoint it; before, they called it a stranger's file and every call kept running the old version. When the old version is gone, the hook says so and prints the ln -sf that fixes it.
    • A validator, formatter or mcp spec whose NAME=value block nothing reads (the key #2734 renamed to variables) is named on every call instead of dropped silently. docs/mcp-integration.md and docs/validators.md teach variables.
    • In the directory build, git-push no longer suggests watch:..., an op that build does not ship; :watch says why it cannot.
  • A directory install no longer starts every session with "unknown operation: channel" (#2734). The directory build drops presets/watch.json, where channel is declared, but hooks/session_start.py still asked for channel:stranded after the op listing, so each session opened on an error and the full list of valid operations. The hook now asks only when the plugin ships presets/watch.json.

  • grep, glob, tree and map skip gitignored files, not only gitignored directories (#2738). Since #449 the walks pruned directories git ignores and searched ignored files anyway. Measured on a scratch repo on 2026-10-04: grep printed an ignored file's contents, glob:* auto-read one, tree listed an ignored file and an ignored directory (it pruned neither), and map extracted def leaked from an ignored .py. Credential files were kept out only by the default exclude list, which stays; .gitignore is now a second layer on top of it. The single git ls-files --others --ignored --exclude-standard --directory each walk already ran answers for files too, so it is still one git call per walk root, never one per file. The header says what was skipped (3 gitignored files hidden), so a filtered search never reads like an empty one. A path named directly is always searched: grep:X:DIR/ on a gitignored directory finds X (unless the exclude list also names it), and grep:X:FILE, glob:FILE and map:FILE read a gitignored file you name, the way rg does. The opt-outs ("gitignore": false, SUPERTOOL_NO_GITIGNORE=1, :no-exclude) are unchanged. Outside a git repository, without git, or when the listing fails, the header says gitignore filter not applied (WHY) rather than showing an unfiltered walk as a filtered one. Inside a repository, grep does not delegate to rtk when an ignored file the exclude list does not cover would be walked, or when the listing failed, because the system grep rtk runs knows nothing about .gitignore; outside one there is nothing to hide, so it delegates and the delegated header carries the same not applied clause. replace walks through the same candidate list as grep, so it skips gitignored files too.