diff --git a/skills/alpha-vantage/SKILL.md b/skills/alpha-vantage/SKILL.md index ed733938..cdd3702d 100644 --- a/skills/alpha-vantage/SKILL.md +++ b/skills/alpha-vantage/SKILL.md @@ -3,7 +3,6 @@ name: alpha-vantage description: Access real-time and historical stock market data, forex rates, cryptocurrency prices, commodities, economic indicators, and 50+ technical indicators via the Alpha Vantage API. Use when fetching stock prices (OHLCV), company fundamentals (income statement, balance sheet, cash... risk: unknown source: community ---- Unknown metadata: skill-author: K-Dense Inc. --- diff --git a/skills/cc-skill-strategic-compact/suggest-compact.sh b/skills/cc-skill-strategic-compact/suggest-compact.sh index ea14920e..d3a7d291 100755 --- a/skills/cc-skill-strategic-compact/suggest-compact.sh +++ b/skills/cc-skill-strategic-compact/suggest-compact.sh @@ -27,8 +27,10 @@ # - Transitioning from research/exploration to implementation # - Plan has been finalized -# Track tool call count (increment in a temp file) -COUNTER_FILE="/tmp/claude-tool-count-$$" +# Track tool call count in a user-owned state directory +COUNTER_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/strategic-compact" +mkdir -p "$COUNTER_DIR" +COUNTER_FILE="$COUNTER_DIR/tool-count" THRESHOLD=${COMPACT_THRESHOLD:-50} # Initialize or increment counter diff --git a/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc b/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc deleted file mode 100644 index 22094ffb..00000000 Binary files a/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc and /dev/null differ diff --git a/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc b/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc deleted file mode 100644 index 3401d980..00000000 Binary files a/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc and /dev/null differ diff --git a/skills/videodb/scripts/ws_listener.py b/skills/videodb/scripts/ws_listener.py index e62fb72b..aac8821c 100644 --- a/skills/videodb/scripts/ws_listener.py +++ b/skills/videodb/scripts/ws_listener.py @@ -56,7 +56,10 @@ def parse_args(): output_dir = arg if output_dir is None: - output_dir = os.environ.get("VIDEODB_EVENTS_DIR", "/tmp") + output_dir = os.environ.get("VIDEODB_EVENTS_DIR") + if output_dir is None: + state_root = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) + output_dir = str(state_root / "videodb-events") return clear, Path(output_dir) diff --git a/tools/scripts/tests/local_temp_safety.test.js b/tools/scripts/tests/local_temp_safety.test.js new file mode 100644 index 00000000..500f955d --- /dev/null +++ b/tools/scripts/tests/local_temp_safety.test.js @@ -0,0 +1,19 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const repoRoot = path.resolve(__dirname, "../..", ".."); + +const compactScript = fs.readFileSync( + path.join(repoRoot, "skills", "cc-skill-strategic-compact", "suggest-compact.sh"), + "utf8", +); +const wsListener = fs.readFileSync( + path.join(repoRoot, "skills", "videodb", "scripts", "ws_listener.py"), + "utf8", +); + +assert.match(compactScript, /XDG_STATE_HOME/, "strategic compact counter should use a user-owned state directory"); +assert.doesNotMatch(compactScript, /\/tmp\/claude-tool-count/, "strategic compact counter must not use predictable /tmp files"); +assert.match(wsListener, /XDG_STATE_HOME/, "videodb listener should default to a user-owned state directory"); +assert.doesNotMatch(wsListener, /VIDEODB_EVENTS_DIR", "\/tmp"/, "videodb listener must not default to /tmp"); diff --git a/tools/scripts/tests/repo_hygiene_security.test.js b/tools/scripts/tests/repo_hygiene_security.test.js new file mode 100644 index 00000000..8398ba01 --- /dev/null +++ b/tools/scripts/tests/repo_hygiene_security.test.js @@ -0,0 +1,22 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const repoRoot = path.resolve(__dirname, "../..", ".."); +const pycacheDir = path.join(repoRoot, "skills", "ui-ux-pro-max", "scripts", "__pycache__"); +const syncRecommended = fs.readFileSync( + path.join(repoRoot, "tools", "scripts", "sync_recommended_skills.sh"), + "utf8", +); +const alphaVantage = fs.readFileSync( + path.join(repoRoot, "skills", "alpha-vantage", "SKILL.md"), + "utf8", +); + +assert.strictEqual( + fs.existsSync(pycacheDir), + false, + "tracked Python bytecode should not ship in skill directories", +); +assert.match(syncRecommended, /cp -RP/, "recommended skills sync should preserve symlinks instead of dereferencing them"); +assert.doesNotMatch(alphaVantage, /--- Unknown/, "alpha-vantage frontmatter should not contain malformed delimiters"); diff --git a/tools/scripts/tests/test_frontmatter_parsing_security.py b/tools/scripts/tests/test_frontmatter_parsing_security.py new file mode 100644 index 00000000..29b7f472 --- /dev/null +++ b/tools/scripts/tests/test_frontmatter_parsing_security.py @@ -0,0 +1,65 @@ +import importlib.util +import sys +import tempfile +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +TOOLS_SCRIPTS_DIR = REPO_ROOT / "tools" / "scripts" +if str(TOOLS_SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(TOOLS_SCRIPTS_DIR)) + + +def load_module(relative_path: str, module_name: str): + module_path = REPO_ROOT / relative_path + spec = importlib.util.spec_from_file_location(module_name, module_path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +generate_index = load_module("tools/scripts/generate_index.py", "generate_index") +validate_skills = load_module("tools/scripts/validate_skills.py", "validate_skills") + + +class FrontmatterParsingSecurityTests(unittest.TestCase): + def test_generate_index_frontmatter_rejects_non_mapping(self): + content = "---\njust-a-string\n---\nbody\n" + metadata = generate_index.parse_frontmatter(content) + + self.assertEqual(metadata, {}) + + def test_validate_skills_frontmatter_rejects_non_mapping(self): + content = "---\njust-a-string\n---\nbody\n" + metadata, errors = validate_skills.parse_frontmatter(content) + + self.assertIsNone(metadata) + self.assertTrue(any("mapping" in error.lower() for error in errors)) + + def test_generate_index_ignores_symlinked_skill_markdown(self): + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + skills_dir = root / "skills" + safe_skill = skills_dir / "safe-skill" + linked_skill = skills_dir / "linked-skill" + outside_dir = root / "outside" + output_file = root / "skills_index.json" + + safe_skill.mkdir(parents=True) + linked_skill.mkdir(parents=True) + outside_dir.mkdir() + + (safe_skill / "SKILL.md").write_text("---\nname: safe-skill\ndescription: safe\n---\n", encoding="utf-8") + target = outside_dir / "SKILL.md" + target.write_text("---\nname: outside\ndescription: outside\n---\n", encoding="utf-8") + (linked_skill / "SKILL.md").symlink_to(target) + + skills = generate_index.generate_index(str(skills_dir), str(output_file)) + + self.assertEqual([skill["id"] for skill in skills], ["safe-skill"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/scripts/validate_skills.py b/tools/scripts/validate_skills.py index 01643235..57edc0b8 100644 --- a/tools/scripts/validate_skills.py +++ b/tools/scripts/validate_skills.py @@ -4,6 +4,7 @@ import argparse import sys import io import yaml +from collections.abc import Mapping from _project_paths import find_repo_root @@ -50,6 +51,8 @@ def parse_frontmatter(content, rel_path=None): fm_errors = [] try: metadata = yaml.safe_load(fm_text) or {} + if not isinstance(metadata, Mapping): + return None, ["Frontmatter must be a YAML mapping/object."] # Identification of the specific regression issue for better reporting if "description" in metadata: @@ -59,7 +62,7 @@ def parse_frontmatter(content, rel_path=None): elif desc == "|": fm_errors.append("description contains only the YAML block indicator '|', likely due to a parsing regression.") - return metadata, fm_errors + return dict(metadata), fm_errors except yaml.YAMLError as e: return None, [f"YAML Syntax Error: {e}"] @@ -86,6 +89,9 @@ def validate_skills(skills_dir, strict_mode=False): if "SKILL.md" in files: skill_count += 1 skill_path = os.path.join(root, "SKILL.md") + if os.path.islink(skill_path): + warnings.append(f"⚠️ {os.path.relpath(skill_path, skills_dir)}: Skipping symlinked SKILL.md") + continue rel_path = os.path.relpath(skill_path, skills_dir) try: