Summary
ConfigManager._get_env_config reads SPECKIT_<EXT_ID>_... environment variables into the current extension's config by prefix-matching on SPECKIT_<EXT_ID>_. But because the underscore is also used as the delimiter between the extension ID and the config path, a longer extension ID whose canonical prefix begins with a shorter extension ID's prefix will leak into the shorter one.
Reproduction
Two extensions co-installed, git and git-hooks (both plausible names — git is a bundled extension in this repo):
import os, tempfile
from pathlib import Path
from specify_cli.extensions import ConfigManager
os.environ["SPECKIT_GIT_URL"] = "value_for_git_ext"
os.environ["SPECKIT_GIT_HOOKS_URL"] = "value_for_git-hooks_ext"
with tempfile.TemporaryDirectory() as td:
cm = ConfigManager(Path(td), "git")
print(cm._get_env_config())
Output (against main, 0.12.13.dev0):
{'url': 'value_for_git_ext', 'hooks': {'url': 'value_for_git-hooks_ext'}}
The git extension has picked up git-hooks's URL variable as hooks.url.
Root cause
src/specify_cli/extensions/__init__.py:2757-2760:
ext_id_upper = self.extension_id.replace("-", "_").upper()
prefix = f"SPECKIT_{ext_id_upper}_"
for key, value in os.environ.items():
if not key.startswith(prefix):
continue
# ...
SPECKIT_GIT_HOOKS_URL starts with SPECKIT_GIT_, so it is accepted as belonging to the git extension. The remainder HOOKS_URL is then interpreted as a nested key path hooks.url.
Impact:
- Config leakage: config intended for one extension surfaces inside another extension's config namespace (
extension.get_config()).
- Behavior corruption via hooks:
_evaluate_condition("config.hooks.url is set", ...) becomes True for the wrong extension, potentially firing hooks that would not otherwise fire.
- Precedence violation: env vars are documented to override YAML config for the extension they target; here they silently override an unrelated extension's YAML.
This is not the same case as #3350 (which fixed intra-extension prefix collisions between two keys of the same extension). This report is about cross-extension leak between different extensions whose IDs share a prefix.
Suggested fix
Skip env vars whose remainder-after-prefix begins with another installed extension's normalized ID + _. This requires reading the installed extensions list (or the registry) at _get_env_config time; a small refactor.
I have a fix + regression tests ready and can open a PR.
Disclosure: this issue and the accompanying PR were prepared with Claude (Anthropic) assistance. Reproducer executed against a fresh clone of main.
Summary
ConfigManager._get_env_configreadsSPECKIT_<EXT_ID>_...environment variables into the current extension's config by prefix-matching onSPECKIT_<EXT_ID>_. But because the underscore is also used as the delimiter between the extension ID and the config path, a longer extension ID whose canonical prefix begins with a shorter extension ID's prefix will leak into the shorter one.Reproduction
Two extensions co-installed,
gitandgit-hooks(both plausible names —gitis a bundled extension in this repo):Output (against
main,0.12.13.dev0):The
gitextension has picked upgit-hooks'sURLvariable ashooks.url.Root cause
src/specify_cli/extensions/__init__.py:2757-2760:SPECKIT_GIT_HOOKS_URLstarts withSPECKIT_GIT_, so it is accepted as belonging to thegitextension. The remainderHOOKS_URLis then interpreted as a nested key pathhooks.url.Impact:
extension.get_config())._evaluate_condition("config.hooks.url is set", ...)becomes True for the wrong extension, potentially firing hooks that would not otherwise fire.This is not the same case as #3350 (which fixed intra-extension prefix collisions between two keys of the same extension). This report is about cross-extension leak between different extensions whose IDs share a prefix.
Suggested fix
Skip env vars whose remainder-after-prefix begins with another installed extension's normalized ID +
_. This requires reading the installed extensions list (or the registry) at_get_env_configtime; a small refactor.I have a fix + regression tests ready and can open a PR.
Disclosure: this issue and the accompanying PR were prepared with Claude (Anthropic) assistance. Reproducer executed against a fresh clone of
main.