{"repo":"NousResearch/hermes-agent","public_id":"630","display_label":"Skill prerequisite checks and dependency surfacing","summary":"These items add prerequisite gating so skills are hidden or blocked when their runtime dependencies are unavailable, and surface missing skill dependencies that were previously not communicated to users.","subsystem":"agent","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":659,"merged_at":"2026-03-08T10:30:09+00:00"},"resolution":{"state":"resolved","by":"pr-659"},"freshness":{"state":"stale","last_verified_at":"2026-03-08T10:30:09+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":630,"pr_number":659,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-08T10:30:09+00:00"},{"issue_number":658,"pr_number":659,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-08T10:30:09+00:00"}],"members":[{"kind":"issue","number":630,"title":"[Bug]: GIF lookup fails — no built-in tool, skill dependency not surfaced","url":null,"state":"closed"},{"kind":"pr","number":659,"title":"feat: skill prerequisites — hide skills with unmet runtime dependencies","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["CONTRIBUTING.md","agent/prompt_builder.py","skills/apple/apple-notes/SKILL.md","skills/apple/apple-reminders/SKILL.md","skills/apple/imessage/SKILL.md","skills/email/himalaya/SKILL.md","skills/feeds/blogwatcher/SKILL.md","skills/gifs/gif-search/SKILL.md","skills/github/codebase-inspection/SKILL.md","skills/mcp/mcporter/SKILL.md","skills/music-creation/songsee/SKILL.md","skills/productivity/notion/SKILL.md","skills/research/duckduckgo-search/SKILL.md","skills/smart-home/openhue/SKILL.md","tests/agent/test_prompt_builder.py","tests/tools/test_skills_tool.py","tools/skills_tool.py"],"fix_hunks":[{"file":"CONTRIBUTING.md","hunks":["@@ -328,6 +328,11 @@ license: MIT\n platforms: [macos, linux]          # Optional — restrict to specific OS platforms\n                                    #   Valid: macos, linux, windows\n                                    #   Omit to load on all platforms (default)\n+prerequisites:                     # Optional — runtime requirements\n+  env_vars: [MY_API_KEY]           #   Env vars that must be set\n+  commands: [curl, jq]             #   CLI binaries that must be on PATH\n+                                   #   Skills with unmet prerequisites are hidden\n+                                   #   from the system prompt and flagged in skill_view.\n metadata:\n   hermes:\n     tags: [Category, Subcategory, Keywords]","@@ -366,6 +371,25 @@ platforms: [windows]          # Windows only\n \n If the field is omitted or empty, the skill loads on all platforms (backward compatible). See `skills/apple/` for examples of macOS-only skills.\n \n+### Skill prerequisites\n+\n+Skills can declare runtime prerequisites via the `prerequisites` frontmatter field. Skills with unmet prerequisites are automatically hidden from the system prompt (the agent won't claim it can use them) and show a clear warning in `skill_view()` telling the agent what's missing.\n+\n+```yaml\n+prerequisites:\n+  env_vars: [TENOR_API_KEY]       # Env vars checked via os.getenv()\n+  commands: [curl, jq]            # CLI binaries checked via shutil.which()\n+```\n+\n+Both sub-fields are optional — declare only what applies. If the field is omitted entirely, the skill is always available (backward compatible).\n+\n+**When to declare prerequisites:**\n+- The skill uses a CLI tool that isn't universally installed (e.g., `himalaya`, `openhue`, `ddgs`)\n+- The skill requires an API key in the environment (e.g., `NOTION_API_KEY`, `TENOR_API_KEY`)\n+- Without these, the skill's commands will fail — not just degrade gracefully\n+\n+See `skills/gifs/gif-search/` and `skills/email/himalaya/` for examples.\n+\n ### Skill guidelines\n \n - **No external dependencies unless absolutely necessary.** Prefer stdlib Python, curl, and existing Hermes tools (`web_extract`, `terminal`, `read_file`)."]},{"file":"agent/prompt_builder.py","hunks":["@@ -170,6 +170,22 @@ def _skill_is_platform_compatible(skill_file: Path) -> bool:\n         return True  # Err on the side of showing the skill\n \n \n+def _skill_prerequisites_met(skill_file: Path) -> bool:\n+    \"\"\"Check if a SKILL.md's declared prerequisites are satisfied.\n+\n+    Returns True (show the skill) when prerequisites are met or not declared.\n+    Returns False when the skill explicitly declares prerequisites that are missing.\n+    \"\"\"\n+    try:\n+        from tools.skills_tool import _parse_frontmatter, check_skill_prerequisites\n+        raw = skill_file.read_text(encoding=\"utf-8\")[:2000]\n+        frontmatter, _ = _parse_frontmatter(raw)\n+        met, _ = check_skill_prerequisites(frontmatter)\n+        return met\n+    except Exception:\n+        return True\n+\n+\n def build_skills_system_prompt() -> str:\n     \"\"\"Build a compact skill index for the system prompt.\n ","@@ -191,6 +207,9 @@ def build_skills_system_prompt() -> str:\n         # Skip skills incompatible with the current OS platform\n         if not _skill_is_platform_compatible(skill_file):\n             continue\n+        # Skip skills whose prerequisites (env vars, commands) are unmet\n+        if not _skill_prerequisites_met(skill_file):\n+            continue\n         rel_path = skill_file.relative_to(skills_dir)\n         parts = rel_path.parts\n         if len(parts) >= 2:"]},{"file":"skills/apple/apple-notes/SKILL.md","hunks":["@@ -9,6 +9,8 @@ metadata:\n   hermes:\n     tags: [Notes, Apple, macOS, note-taking]\n     related_skills: [obsidian]\n+prerequisites:\n+  commands: [memo]\n ---\n \n # Apple Notes"]},{"file":"skills/apple/apple-reminders/SKILL.md","hunks":["@@ -8,6 +8,8 @@ platforms: [macos]\n metadata:\n   hermes:\n     tags: [Reminders, tasks, todo, macOS, Apple]\n+prerequisites:\n+  commands: [remindctl]\n ---\n \n # Apple Reminders"]},{"file":"skills/apple/imessage/SKILL.md","hunks":["@@ -8,6 +8,8 @@ platforms: [macos]\n metadata:\n   hermes:\n     tags: [iMessage, SMS, messaging, macOS, Apple]\n+prerequisites:\n+  commands: [imsg]\n ---\n \n # iMessage"]},{"file":"skills/email/himalaya/SKILL.md","hunks":["@@ -8,6 +8,8 @@ metadata:\n   hermes:\n     tags: [Email, IMAP, SMTP, CLI, Communication]\n     homepage: https://github.com/pimalaya/himalaya\n+prerequisites:\n+  commands: [himalaya]\n ---\n \n # Himalaya Email CLI"]},{"file":"skills/feeds/blogwatcher/SKILL.md","hunks":["@@ -8,6 +8,8 @@ metadata:\n   hermes:\n     tags: [RSS, Blogs, Feed-Reader, Monitoring]\n     homepage: https://github.com/Hyaxia/blogwatcher\n+prerequisites:\n+  commands: [blogwatcher]\n ---\n \n # Blogwatcher"]},{"file":"skills/gifs/gif-search/SKILL.md","hunks":["@@ -1,9 +1,12 @@\n ---\n name: gif-search\n description: Search and download GIFs from Tenor using curl. No dependencies beyond curl and jq. Useful for finding reaction GIFs, creating visual content, and sending GIFs in chat.\n-version: 1.0.0\n+version: 1.1.0\n author: Hermes Agent\n license: MIT\n+prerequisites:\n+  env_vars: [TENOR_API_KEY]\n+  commands: [curl, jq]\n metadata:\n   hermes:\n     tags: [GIF, Media, Search, Tenor, API]","@@ -13,32 +16,43 @@ metadata:\n \n Search and download GIFs directly via the Tenor API using curl. No extra tools needed.\n \n+## Setup\n+\n+Set your Tenor API key in your environment (add to `~/.hermes/.env`):\n+\n+```bash\n+TENOR_API_KEY=[redacted:secret-assignment]\n+```\n+\n+Get a free API key at https://developers.google.com/tenor/guides/quickstart — the Google Cloud Console Tenor API key is free and has generous rate limits.\n+\n ## Prerequisites\n \n-- `curl` and `jq` (both standard on Linux)\n+- `curl` and `jq` (both standard on macOS/Linux)\n+- `TENOR_API_KEY` environment variable\n \n ## Search for GIFs\n \n ```bash\n # Search and get GIF URLs\n-curl -s \"https://tenor.googleapis.com/v2/search?q=thumbs+up&limit=5&key=[redacted:google-api-key]\" | jq -r '.results[].media_formats.gif.url'\n+curl -s \"https://tenor.googleapis.com/v2/search?q=thumbs+up&limit=5&key=${TENOR_API_KEY}\" | jq -r '.results[].media_formats.gif.url'\n \n # Get smaller/preview versions\n-curl -s \"https://tenor.googleapis.com/v2/search?q=nice+work&limit=3&key=[redacted:google-api-key]\" | jq -r '.results[].media_formats.tinygif.url'\n+curl -s \"https://tenor.googleapis.com/v2/search?q=nice+work&limit=3&key=${TENOR_API_KEY}\" | jq -r '.results[].media_formats.tinygif.url'\n ```\n \n ## Download a GIF\n \n ```bash\n # Search and download the top result\n-URL=$(curl -s \"https://tenor.googleapis.com/v2/search?q=celebration&limit=1&key=[redacted:google-api-key]\" | jq -r '.results[0].media_formats.gif.url')\n+URL=$(curl -s \"https://tenor.googleapis.com/v2/search?q=celebration&limit=1&key=${TENOR_API_KEY}\" | jq -r '.results[0].media_formats.gif.url')\n curl -sL \"$URL\" -o celebration.gif\n ```\n \n ## Get Full Metadata\n \n ```bash\n-curl -s \"https://tenor.googleapis.com/v2/search?q=cat&limit=3&key=[redacted:google-api-key]\" | jq '.results[] | {title: .title, url: .media_formats.gif.url, preview: .media_formats.tinygif.url, dimensions: .media_formats.gif.dims}'\n+curl -s \"https://tenor.googleapis.com/v2/search?q=cat&limit=3&key=${TENOR_API_KEY}\" | jq '.results[] | {title: .title, url: .media_formats.gif.url, preview: .media_formats.tinygif.url, dimensions: .media_formats.gif.dims}'\n ```\n \n ## API Parameters","@@ -47,7 +61,7 @@ curl -s \"https://tenor.googleapis.com/v2/search?q=cat&limit=3&key=AIzaSyAyimkuYQ\n |-----------|-------------|\n | `q` | Search query (URL-encode spaces as `+`) |\n | `limit` | Max results (1-50, default 20) |\n-| `key` | API key (the one above is Tenor's public demo key) |\n+| `key` | API key (from `$TENOR_API_KEY` env var) |\n | `media_filter` | Filter formats: `gif`, `tinygif`, `mp4`, `tinymp4`, `webm` |\n | `contentfilter` | Safety: `off`, `low`, `medium`, `high` |\n | `locale` | Language: `en_US`, `es`, `fr`, etc. |","@@ -67,7 +81,6 @@ Each result has multiple formats under `.media_formats`:\n \n ## Notes\n \n-- The API key above is Tenor's public demo key — it works but has rate limits\n - URL-encode the query: spaces as `+`, special chars as `%XX`\n - For sending in chat, `tinygif` URLs are lighter weight\n - GIF URLs can be used directly in markdown: `![alt](url)`"]},{"file":"skills/github/codebase-inspection/SKILL.md","hunks":["@@ -8,6 +8,8 @@ metadata:\n   hermes:\n     tags: [LOC, Code Analysis, pygount, Codebase, Metrics, Repository]\n     related_skills: [github-repo-management]\n+prerequisites:\n+  commands: [pygount]\n ---\n \n # Codebase Inspection with pygount"]},{"file":"skills/mcp/mcporter/SKILL.md","hunks":["@@ -8,6 +8,8 @@ metadata:\n   hermes:\n     tags: [MCP, Tools, API, Integrations, Interop]\n     homepage: https://mcporter.dev\n+prerequisites:\n+  commands: [npx]\n ---\n \n # mcporter"]},{"file":"skills/music-creation/songsee/SKILL.md","hunks":["@@ -8,6 +8,8 @@ metadata:\n   hermes:\n     tags: [Audio, Visualization, Spectrogram, Music, Analysis]\n     homepage: https://github.com/steipete/songsee\n+prerequisites:\n+  commands: [songsee]\n ---\n \n # songsee"]},{"file":"skills/productivity/notion/SKILL.md","hunks":["@@ -8,6 +8,8 @@ metadata:\n   hermes:\n     tags: [Notion, Productivity, Notes, Database, API]\n     homepage: https://developers.notion.com\n+prerequisites:\n+  env_vars: [NOTION_API_KEY]\n ---\n \n # Notion API"]},{"file":"skills/research/duckduckgo-search/SKILL.md","hunks":["@@ -8,6 +8,8 @@ metadata:\n   hermes:\n     tags: [search, duckduckgo, web-search, free, fallback]\n     related_skills: [arxiv]\n+prerequisites:\n+  commands: [ddgs]\n ---\n \n # DuckDuckGo Search (Firecrawl Fallback)"]},{"file":"skills/smart-home/openhue/SKILL.md","hunks":["@@ -8,6 +8,8 @@ metadata:\n   hermes:\n     tags: [Smart-Home, Hue, Lights, IoT, Automation]\n     homepage: https://www.openhue.io/cli\n+prerequisites:\n+  commands: [openhue]\n ---\n \n # OpenHue CLI"]},{"file":"tests/agent/test_prompt_builder.py","hunks":["@@ -8,6 +8,7 @@\n     _scan_context_content,\n     _truncate_content,\n     _read_skill_description,\n+    _skill_prerequisites_met,\n     build_skills_system_prompt,\n     build_context_files_prompt,\n     CONTEXT_FILE_MAX_CHARS,","@@ -211,6 +212,69 @@ def test_includes_matching_platform_skills(self, monkeypatch, tmp_path):\n         assert \"imessage\" in result\n         assert \"Send iMessages\" in result\n \n+    def test_excludes_skills_with_unmet_prerequisites(self, monkeypatch, tmp_path):\n+        \"\"\"Skills with missing env var prerequisites should not appear.\"\"\"\n+        monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n+        monkeypatch.delenv(\"MISSING_API_KEY_XYZ\", raising=False)\n+        skills_dir = tmp_path / \"skills\" / \"media\"\n+\n+        gated = skills_dir / \"gated-skill\"\n+        gated.mkdir(parents=True)\n+        (gated / \"SKILL.md\").write_text(\n+            \"---\\nname: gated-skill\\ndescription: Needs a key\\n\"\n+            \"prerequisites:\\n  env_vars: [MISSING_API_KEY_XYZ]\\n---\\n\"\n+        )\n+\n+        available = skills_dir / \"free-skill\"\n+        available.mkdir(parents=True)\n+        (available / \"SKILL.md\").write_text(\n+            \"---\\nname: free-skill\\ndescription: No prereqs\\n---\\n\"\n+        )\n+\n+        result = build_skills_system_prompt()\n+        assert \"free-skill\" in result\n+        assert \"gated-skill\" not in result\n+\n+    def test_includes_skills_with_met_prerequisites(self, monkeypatch, tmp_path):\n+        \"\"\"Skills with satisfied prerequisites should appear normally.\"\"\"\n+        monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n+        monkeypatch.setenv(\"MY_API_KEY\", \"test_value\")\n+        skills_dir = tmp_path / \"skills\" / \"media\"\n+\n+        skill = skills_dir / \"ready-skill\"\n+        skill.mkdir(parents=True)\n+        (skill / \"SKILL.md\").write_text(\n+            \"---\\nname: ready-skill\\ndescription: Has key\\n\"\n+            \"prerequisites:\\n  env_vars: [MY_API_KEY]\\n---\\n\"\n+        )\n+\n+        result = build_skills_system_prompt()\n+        assert \"ready-skill\" in result\n+\n+\n+# =========================================================================\n+# _skill_prerequisites_met\n+# =========================================================================\n+\n+\n+class TestSkillPrerequisitesMet:\n+    def test_met_or_absent(self, tmp_path, monkeypatch):\n+        \"\"\"No prereqs, met prereqs, and missing file all return True.\"\"\"\n+        monkeypatch.setenv(\"PRESENT_KEY_123\", \"val\")\n+        basic = tmp_path / \"basic.md\"\n+        basic.write_text(\"---\\nname: basic\\ndescription: basic\\n---\\n\")\n+        ready = tmp_path / \"ready.md\"\n+        ready.write_text(\"---\\nname: ready\\ndescription: ready\\nprerequisites:\\n  env_vars: [PRESENT_KEY_123]\\n---\\n\")\n+        assert _skill_prerequisites_met(basic) is True\n+        assert _skill_prerequisites_met(ready) is True\n+        assert _skill_prerequisites_met(tmp_path / \"nope.md\") is True\n+\n+    def test_unmet_returns_false(self, tmp_path, monkeypatch):\n+        monkeypatch.delenv(\"NONEXISTENT_KEY_ABC\", raising=False)\n+        skill = tmp_path / \"SKILL.md\"\n+        skill.write_text(\"---\\nname: gated\\ndescription: gated\\nprerequisites:\\n  env_vars: [NONEXISTENT_KEY_ABC]\\n---\\n\")\n+        assert _skill_prerequisites_met(skill) is False\n+\n \n # =========================================================================\n # Context files prompt builder"]},{"file":"tests/tools/test_skills_tool.py","hunks":["@@ -11,6 +11,7 @@\n     _estimate_tokens,\n     _find_all_skills,\n     _load_category_description,\n+    check_skill_prerequisites,\n     skill_matches_platform,\n     skills_list,\n     skills_categories,","@@ -464,3 +465,124 @@ def test_multi_platform_skill(self, tmp_path):\n         assert len(skills_darwin) == 1\n         assert len(skills_linux) == 1\n         assert len(skills_win) == 0\n+\n+\n+# ---------------------------------------------------------------------------\n+# check_skill_prerequisites\n+# ---------------------------------------------------------------------------\n+\n+\n+class TestCheckSkillPrerequisites:\n+    def test_no_or_empty_prerequisites(self):\n+        \"\"\"No field, empty dict, or non-dict all pass.\"\"\"\n+        assert check_skill_prerequisites({})[0] is True\n+        assert check_skill_prerequisites({\"prerequisites\": {}})[0] is True\n+        assert check_skill_prerequisites({\"prerequisites\": \"curl\"})[0] is True\n+\n+    def test_env_var_present_and_missing(self, monkeypatch):\n+        monkeypatch.setenv(\"MY_TEST_KEY\", \"val\")\n+        monkeypatch.delenv(\"NONEXISTENT_TEST_VAR_XYZ\", raising=False)\n+        assert check_skill_prerequisites({\"prerequisites\": {\"env_vars\": [\"MY_TEST_KEY\"]}})[0] is True\n+        met, missing = check_skill_prerequisites({\"prerequisites\": {\"env_vars\": [\"NONEXISTENT_TEST_VAR_XYZ\"]}})\n+        assert met is False\n+        assert \"env $NONEXISTENT_TEST_VAR_XYZ\" in missing\n+\n+    def test_command_present_and_missing(self):\n+        assert check_skill_prerequisites({\"prerequisites\": {\"commands\": [\"python3\"]}})[0] is True\n+        met, missing = check_skill_prerequisites({\"prerequisites\": {\"commands\": [\"nonexistent_binary_xyz_123\"]}})\n+        assert met is False\n+        assert \"command `nonexistent_binary_xyz_123`\" in missing\n+\n+    def test_mixed_env_and_commands(self, monkeypatch):\n+        monkeypatch.delenv(\"MISSING_A\", raising=False)\n+        met, missing = check_skill_prerequisites({\n+            \"prerequisites\": {\n+                \"env_vars\": [\"MISSING_A\"],\n+                \"commands\": [\"python3\", \"nonexistent_cmd_xyz\"],\n+            }\n+        })\n+        assert met is False\n+        assert len(missing) == 2\n+\n+    def test_string_instead_of_list(self, monkeypatch):\n+        \"\"\"YAML scalar (string) should be coerced to a single-element list.\"\"\"\n+        monkeypatch.delenv(\"SOLO_VAR\", raising=False)\n+        assert check_skill_prerequisites({\"prerequisites\": {\"env_vars\": \"SOLO_VAR\"}})[0] is False\n+        assert check_skill_prerequisites({\"prerequisites\": {\"commands\": \"nonexistent_cmd_xyz_solo\"}})[0] is False\n+\n+\n+# ---------------------------------------------------------------------------\n+# _find_all_skills — prerequisites integration\n+# ---------------------------------------------------------------------------\n+\n+\n+class TestFindAllSkillsPrerequisites:\n+    def test_skills_with_unmet_prereqs_flagged(self, tmp_path, monkeypatch):\n+        monkeypatch.delenv(\"NONEXISTENT_API_KEY_XYZ\", raising=False)\n+        with patch(\"tools.skills_tool.SKILLS_DIR\", tmp_path):\n+            _make_skill(\n+                tmp_path, \"needs-key\",\n+                frontmatter_extra=\"prerequisites:\\n  env_vars: [NONEXISTENT_API_KEY_XYZ]\\n\",\n+            )\n+            skills = _find_all_skills()\n+        assert len(skills) == 1\n+        assert skills[0][\"prerequisites_met\"] is False\n+        assert any(\"NONEXISTENT_API_KEY_XYZ\" in m for m in skills[0][\"prerequisites_missing\"])\n+\n+    def test_skills_with_met_prereqs_no_flag(self, tmp_path, monkeypatch):\n+        monkeypatch.setenv(\"MY_PRESENT_KEY\", \"val\")\n+        with patch(\"tools.skills_tool.SKILLS_DIR\", tmp_path):\n+            _make_skill(\n+                tmp_path, \"has-key\",\n+                frontmatter_extra=\"prerequisites:\\n  env_vars: [MY_PRESENT_KEY]\\n\",\n+            )\n+            skills = _find_all_skills()\n+        assert len(skills) == 1\n+        assert \"prerequisites_met\" not in skills[0]\n+\n+    def test_skills_without_prereqs_no_flag(self, tmp_path):\n+        with patch(\"tools.skills_tool.SKILLS_DIR\", tmp_path):\n+            _make_skill(tmp_path, \"simple-skill\")\n+            skills = _find_all_skills()\n+        assert len(skills) == 1\n+        assert \"prerequisites_met\" not in skills[0]\n+\n+\n+# ---------------------------------------------------------------------------\n+# skill_view — prerequisites warnings\n+# ---------------------------------------------------------------------------\n+\n+\n+class TestSkillViewPrerequisites:\n+    def test_warns_on_unmet_prerequisites(self, tmp_path, monkeypatch):\n+        monkeypatch.delenv(\"MISSING_KEY_XYZ\", raising=False)\n+        with patch(\"tools.skills_tool.SKILLS_DIR\", tmp_path):\n+            _make_skill(\n+                tmp_path, \"gated-skill\",\n+                frontmatter_extra=\"prerequisites:\\n  env_vars: [MISSING_KEY_XYZ]\\n\",\n+            )\n+            raw = skill_view(\"gated-skill\")\n+        result = json.loads(raw)\n+        assert result[\"success\"] is True\n+        assert result[\"prerequisites_met\"] is False\n+        assert \"MISSING_KEY_XYZ\" in result[\"prerequisites_warning\"]\n+\n+    def test_no_warning_when_prereqs_met(self, tmp_path, monkeypatch):\n+        monkeypatch.setenv(\"PRESENT_KEY\", \"value\")\n+        with patch(\"tools.skills_tool.SKILLS_DIR\", tmp_path):\n+            _make_skill(\n+                tmp_path, \"ready-skill\",\n+                frontmatter_extra=\"prerequisites:\\n  env_vars: [PRESENT_KEY]\\n\",\n+            )\n+            raw = skill_view(\"ready-skill\")\n+        result = json.loads(raw)\n+        assert result[\"success\"] is True\n+        assert \"prerequisites_warning\" not in result\n+\n+    def test_no_warning_when_no_prereqs(self, tmp_path):\n+        with patch(\"tools.skills_tool.SKILLS_DIR\", tmp_path):\n+            _make_skill(tmp_path, \"plain-skill\")\n+            raw = skill_view(\"plain-skill\")\n+        result = json.loads(raw)\n+        assert result[\"success\"] is True\n+        assert \"prerequisites_warning\" not in result"]},{"file":"tools/skills_tool.py","hunks":["@@ -34,6 +34,11 @@\n     platforms: [macos]            # Optional — restrict to specific OS platforms\n                                   #   Valid: macos, linux, windows\n                                   #   Omit to load on all platforms (default)\n+    prerequisites:                # Optional — runtime requirements\n+      env_vars: [API_KEY]         #   Env vars that must be set (checked via os.getenv)\n+      commands: [curl, jq]        #   CLI binaries that must be on PATH (checked via shutil.which)\n+                                  #   Skills with unmet prerequisites are hidden from the\n+                                  #   system prompt and flagged with a warning in skill_view.\n     compatibility: Requires X     # Optional (agentskills.io)\n     metadata:                     # Optional, arbitrary key-value (agentskills.io)\n       hermes:","@@ -65,6 +70,7 @@\n import json\n import os\n import re\n+import shutil\n import sys\n from pathlib import Path\n from typing import Dict, Any, List, Optional, Tuple","@@ -118,6 +124,43 @@ def skill_matches_platform(frontmatter: Dict[str, Any]) -> bool:\n     return False\n \n \n+def check_skill_prerequisites(frontmatter: Dict[str, Any]) -> Tuple[bool, List[str]]:\n+    \"\"\"Check if a skill's declared prerequisites are satisfied.\n+\n+    Skills declare prerequisites via a top-level ``prerequisites`` dict\n+    in their YAML frontmatter::\n+\n+        prerequisites:\n+          env_vars: [TENOR_API_KEY]\n+          commands: [curl, jq]\n+\n+    Returns:\n+        (all_met, missing) — True + empty list if all met, else False + list\n+        of human-readable descriptions of what's missing.\n+    \"\"\"\n+    prereqs = frontmatter.get(\"prerequisites\")\n+    if not prereqs or not isinstance(prereqs, dict):\n+        return True, []\n+\n+    missing: List[str] = []\n+\n+    env_vars = prereqs.get(\"env_vars\") or []\n+    if isinstance(env_vars, str):\n+        env_vars = [env_vars]\n+    for var in env_vars:\n+        if not os.getenv(str(var)):\n+            missing.append(f\"env ${var}\")\n+\n+    commands = prereqs.get(\"commands\") or []\n+    if isinstance(commands, str):\n+        commands = [commands]\n+    for cmd in commands:\n+        if not shutil.which(str(cmd)):\n+            missing.append(f\"command `{cmd}`\")\n+\n+    return (len(missing) == 0), missing\n+\n+\n def check_skills_requirements() -> bool:\n     \"\"\"Skills are always available -- the directory is created on first use if needed.\"\"\"\n     return True","@@ -262,12 +305,19 @@ def _find_all_skills() -> List[Dict[str, Any]]:\n                 description = description[:MAX_DESCRIPTION_LENGTH - 3] + \"...\"\n             \n             category = _get_category_from_path(skill_md)\n-            \n-            skills.append({\n+\n+            prereqs_met, prereqs_missing = check_skill_prerequisites(frontmatter)\n+\n+            entry = {\n                 \"name\": name,\n                 \"description\": description,\n                 \"category\": category,\n-            })\n+            }\n+            if not prereqs_met:\n+                entry[\"prerequisites_met\"] = False\n+                entry[\"prerequisites_missing\"] = prereqs_missing\n+\n+            skills.append(entry)\n             \n         except Exception:\n             continue","@@ -635,6 +685,17 @@ def skill_view(name: str, file_path: str = None, task_id: str = None) -> str:\n             \"usage_hint\": \"To view linked files, call skill_view(name, file_path) where file_path is e.g. 'references/api.md' or 'assets/config.yaml'\" if linked_files else None\n         }\n         \n+        # Prerequisite check — warn the agent if requirements are unmet\n+        prereqs_met, prereqs_missing = check_skill_prerequisites(frontmatter)\n+        if not prereqs_met:\n+            result[\"prerequisites_met\"] = False\n+            result[\"prerequisites_missing\"] = prereqs_missing\n+            result[\"prerequisites_warning\"] = (\n+                f\"This skill requires {', '.join(prereqs_missing)} which \"\n+                f\"{'is' if len(prereqs_missing) == 1 else 'are'} not available. \"\n+                f\"Tell the user what's needed before attempting to use this skill.\"\n+            )\n+\n         # Surface agentskills.io optional fields when present\n         if frontmatter.get('compatibility'):\n             result[\"compatibility\"] = frontmatter['compatibility']\n"]}]},{"kind":"pr","number":664,"title":"fix: gate skill-backed claims behind available skills and prerequisites","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#659","number":659}],"category":"merged_via","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}