TL;DR — The official docs teach you how to write SKILL.md, but don’t cover the experience after the Skill is running. The main thread of this post is how to design good tool interfaces for agents (layered models, script design, lifecycle, environment management), plus a set of UX tips for end users. These practices are packaged as better-skills — install with
npx skills add psylch/better-skills -g -y.
Introduction: From “How to Write Skills” to “Optimizing the Skills User Experience”
Anthropic’s official documentation does a solid job on the authoring side of Skills. Whether it’s the Skill Creator or the Best Practices guide, the guidance is clear: keep SKILL.md concise, use progressive disclosure to manage context, scripts should “solve, don’t punt”, and use feedback loops to ensure quality. This advice is sound.
But after building over a dozen Skills of different types — covering terminal environment setup, multi-source technical research, music downloads, ebook search, cloud drive resources, DNS management, and CORS auditing — I discovered a systematic blind spot in the official docs: they barely discuss the runtime user experience after a Skill is installed and actually being used.
Specifically, you won’t find answers to these questions in the official documentation:
- When a Skill needs Python dependencies, should you use system pip, a per-skill venv, or something else? What happens when multiple Skills have conflicting dependency versions?
- When a user triggers a Skill for the first time and the environment isn’t ready, how should the agent guide them?
- When an external service that a Skill depends on is unavailable, should it error out or degrade gracefully?
- The scripts inside a Skill are CLI tools called by the agent — what principles should their output format, error handling, and subcommand design follow?
The first five sections of this post discuss how to design tool interfaces for agents: output formats, lifecycle, and environment isolation. The sixth section covers UX tips for end users. The seventh briefly touches on security — I haven’t done systematic research here, but real-world attacks have already occurred, and basic code hygiene is worth mentioning.
Part 1: The Skill Layering Model
Not all Skills need scripts, and not all Skills with scripts need complex environment management. Before diving into specific practices, let’s establish three tiers of Skills, since different tiers face fundamentally different problems.
L0 Pure Prompt Skills consist of just a SKILL.md, with all capabilities handled by the agent itself. The agent uses built-in tools (WebSearch, file I/O, bash commands) or configured MCP servers to execute tasks — the Skill contains no executable code. This is the lightest form and the easiest entry point for most Skill authors — you don’t need to code, just articulate the workflow and domain knowledge clearly.
A typical example is my ghostty-vibe-coding-stack skill: a 12-step terminal environment setup wizard where all installation is done by the agent running brew install and writing config files. The SKILL.md contains only step-by-step guidance and reference configurations.
L0+ Prompt + Helper Scripts still have their core logic in the SKILL.md prompt, but include a small number of helper scripts for environment detection, state caching, and other tasks that the agent can’t easily do or shouldn’t rewrite every time. The scripts don’t carry business logic — they’re purely auxiliary.
A typical example is tech-research-skill: the orchestration logic for multi-source research (choosing Light/Heavy mode, dispatching subagents, synthesizing reports) lives entirely in SKILL.md, while grok_setup.sh only handles detecting the browser backend and caching login status.
L1 Prompt + Business Scripts have their core functionality carried by scripts, with SKILL.md responsible for orchestrating calls. Scripts receive parameters, execute operations, and return structured results; Claude decides next steps based on the results. The scripts in these Skills are CLI tools for AI — MCP’s tool design principles apply directly here.
Typical examples include hifi-download, zlib-download, quark-download, cors-audit, and cloudflare-dns.
The significance of layering: L0 Skills don’t need to worry about environment issues, L0+ only needs minimal script runtime, and L1 requires systematic handling of dependencies, environment, output formats, and more. The best practices that follow primarily target L0+ and L1.
Part 2: Script Design — CLI Tools for AI
When you decide to include scripts in a Skill, you’re essentially designing a CLI tool interface for an AI agent. The consumer of this interface isn’t a human user — it’s Claude. It calls your script via bash, reads results from stdout, and judges errors through stderr and exit codes.
From another angle, scripted Skills and MCP Tools do the same thing — receive instructions from an AI agent, execute operations, and return results — just with different transport layers. MCP Tools use JSON-RPC; Skill scripts use bash invocation + stdout/stderr. This means mature tool design principles from the MCP ecosystem can be directly transferred. Anthropic systematically summarized these principles in Writing effective tools for agents, but they’re completely absent from the Skill documentation — the official Skill Best Practices gives only one piece of advice on script design: “solve, don’t punt.” That’s correct, but far from sufficient.
Here’s how I’ve applied these principles to Skill scripts in practice.
Output Format: JSON + Natural Language Hints
Script stdout should be JSON, because the agent needs to parse structured data for decision-making. But don’t just return raw data — include natural language hints in key fields to help the agent relay information to users more effectively.
{
"status": "ok",
"results": [
{"title": "Deep Learning", "author": "Ian Goodfellow", "format": "PDF", "size": "22MB"}
],
"hint": "Found 1 matching book, PDF format, 22MB"
}
Avoid key=value plain text output (like BACKEND=chrome\nLOGIN_STATUS=logged_in). This format requires the agent to do its own string parsing, which is both unreliable and wasteful of tokens. Even if the script is simple enough to return a single status value, JSON is still the better choice — {"ready": true} is less likely to be misparsed than READY=true.
Error messages go to stderr, also in JSON format:
{"error": "zlib_login_failed", "hint": "Z-Library login failed. Check the email and password in ~/.claude/zlib-download/.env"}
Subcommand Design: A Unified Verb Vocabulary
A Skill script typically contains multiple subcommands. From my experience across multiple Skills, these are the recurring universal subcommands:
preflight checks whether the current environment meets runtime requirements and returns standardized readiness status. setup performs first-time installation and configuration. status shows current runtime state (configured services, login status, etc.). Then come business commands — search, download, validate, list, etc., depending on the specific Skill’s functionality.
The key point is that preflight and setup naming should be consistent across all Skills. When the agent encounters a new Skill, it should be able to expect that python3 scripts/main.py preflight will return environment status, without needing to look up different command names in each SKILL.md (some call it check, others status, others verify).
Error Handling: Actionable Information, Meaningful Exit Codes
Perhaps the most valuable principle from MCP best practices is “actionable error messages.” For Skill scripts, this means:
Don’t just tell the agent “something went wrong” — tell it “what went wrong, why, and what to do next.”
# Bad
sys.exit("Error: authentication failed")
# Good
error = {
"error": "spotify_auth_expired",
"hint": "Spotify OAuth token has expired. Run 'bash run.sh spotify_auth' to re-authorize",
"recoverable": True
}
print(json.dumps(error), file=sys.stderr)
sys.exit(1)
The recoverable field is particularly important — it tells the agent whether this error can be automatically recovered through some action, or whether the user must be notified to intervene manually. This directly affects whether the agent’s actions during complex tasks will degrade the environment state.
Exit codes should also follow a unified convention: 0 for success, 1 for recoverable errors (the agent can attempt to fix), 2 for unrecoverable errors (user intervention required).
Token Awareness: Don’t Dump Everything at Once
This is a point the MCP documentation emphasizes repeatedly. When Skill scripts return large amounts of data, they should support a --limit parameter to control the number of returned entries and --format concise vs --format detailed to toggle output verbosity.
The default should return the concise format. The agent can re-invoke with --format detailed when more detail is needed. This is far more efficient than returning all information at once and letting the agent filter — every unnecessary token consumes context window space.
Part 3: Runtime Lifecycle
Every scripted Skill goes through three phases at runtime: preflight, setup, and run. Even if some phases are no-ops (e.g., a stdlib-only Skill doesn’t need setup), this lifecycle model should remain consistent across all Skills, allowing the agent to form stable interaction expectations.
Preflight: Standardized Environment Probing
Preflight is the agent’s first action before executing a Skill’s business logic. Its purpose is to quickly answer one question: Can this Skill run right now?
All Skills’ preflight should return a JSON with a unified structure:
{
"ready": true,
"dependencies": {
"python3": {"status": "ok", "version": "3.12.0"},
"uv": {"status": "ok", "version": "0.6.0"}
},
"credentials": {
"spotify": {"status": "configured"},
"qobuz": {"status": "not_configured", "hint": "Edit ~/.claude/hifi-download/.env to add QOBUZ_EMAIL and QOBUZ_PASSWORD", "required": false}
},
"services": {
"quark_app": {"status": "running", "version": "6.12.0"}
}
}
ready: true means core functionality is available and the agent can proceed directly to the business flow. When ready: false, the agent reads the hint fields to guide the user through completing the missing configuration.
Note the required field in credentials — not all credentials are mandatory. hifi-download’s Spotify is required for core functionality, but Qobuz and TIDAL are optional enhancements. Preflight should distinguish between “can’t work without it” and “just one fewer data source.”
Setup: First-Run Guidance
Setup only executes when preflight reports missing items. It should be divided into what Claude can complete automatically and what requires user action.
Claude can handle automatically: installing dependencies (uv sync, pip install), creating directory structures, generating config file templates.
Requires user action: filling in API credentials, logging into external services, launching desktop applications.
SKILL.md should clearly indicate which steps are automatic and which need user involvement. Claude should never directly ask for passwords or API keys in chat — it should guide users to edit config files. This is both a security consideration (chat logs may be saved) and a better user experience (users can use their preferred editor).
Run: Business Execution
Once preflight confirms readiness, Claude follows the orchestration logic in SKILL.md to call business commands. The key design decision at this stage is the degradation strategy — what to do when some capabilities are unavailable.
From my practice, I’ve identified four degradation patterns:
Skip and report: tech-research skips the Twitter data source when Grok is unavailable, completing research with just DeepWiki + WebSearch, and notes in the final report that “Grok data source was skipped due to expired login status.” This is the most common pattern.
Automatic fallback: zlib-download automatically switches to the Anna’s Archive backend when Z-Library is unavailable. The user doesn’t even need to know what happened.
Disable individual services: hifi-download allows explicitly disabling a music platform (e.g., the user doesn’t have a TIDAL membership), while other platforms continue to work.
Cannot degrade, terminate and guide: quark-download can’t work when the Quark desktop app isn’t running — this is a hard dependency with no alternative. In this case, it should directly tell the user “Please launch the Quark desktop app” rather than retrying repeatedly.
SKILL.md should annotate each external dependency with its degradation mode, so Claude can make reasonable decisions when encountering issues rather than getting stuck.
Part 4: Environment Management Strategy
This is what I consider the weakest link in the current Skill ecosystem. When multiple Skills each install their own Python dependencies, version conflicts, disk usage, and first-install latency all become problems. My recommendation is a tiered strategy — if a lightweight solution works, never use a heavyweight one.
First Priority: Have No Dependencies
The Python standard library is far more capable than most people realize. urllib.request can make HTTP requests, json can parse and generate JSON, argparse can handle command-line arguments, pathlib can manipulate file paths, and subprocess can call external commands.
My cors-audit and quark-download both demonstrate the viability of pure stdlib — the former implements complete CORS auditing (including HTTP requests, config file parsing, and report generation), while the latter implements cloud drive resource search and concurrent verification. Zero dependencies means zero environment issues.
When you feel the need to bring in an external library, first ask yourself: can stdlib really not do this? In many cases, what requests can do, urllib.request can do too, just slightly more verbose. Trading that small convenience for an entire dependency chain is usually not worth it in the Skill context.
Second Priority: Vendoring
If you genuinely need an external library and it’s pure Python (no C extensions), you can place its source files directly into the Skill directory. My zlib-download actually does this — Zlibrary.py is a vendored Z-Library API client.
The benefit of vendoring is completely eliminating the runtime installation step. The downside is manual updates. For the Skill context, dependencies rarely change, so manual updates are acceptable.
Third Priority: uv run + Inline Dependency Declarations
When the dependency chain is complex or includes libraries that need compilation, uv provides an elegant solution. uv can run Python scripts with dependencies without creating a persistent venv — dependencies are cached globally, shared across Skills but isolated from each other.
Combined with PEP 723 inline metadata, you can declare dependencies directly in the script header:
# /// script
# requires-python = ">=3.11"
# dependencies = ["requests>=2.28", "spotipy>=2.23"]
# ///
import requests
import spotipy
Then run with uv run scripts/search.py. uv automatically reads the inline declarations, resolves dependencies, and runs in an isolated environment — leaving no .venv directory in the Skill directory and not polluting system Python.
One benefit of this approach is that dependency declarations and code live in the same file, preventing requirements.txt from drifting out of sync with actual code. Another benefit is that uv’s installation speed is extremely fast (10-100x faster than pip), dramatically improving the first-run waiting experience.
uv itself only needs a one-time install (brew install uv or curl -LsSf https://astral.sh/uv/install.sh | sh) and can serve as a universal prerequisite for the entire Skill ecosystem, much like npm in the Node.js ecosystem.
Fourth Priority: Per-Skill venv
Traditional per-skill venvs are only recommended when: the dependency chain is particularly complex, includes C extensions that need compilation (like numpy, Pillow), or the Skill needs to run a long-lived process (like hifi-download’s download progress dashboard).
The venv should be created within the Skill’s own directory (scripts/.venv/ or .venv/), with a run.sh wrapper handling the activation process, so callers only need bash run.sh <command> without knowing venv details.
Fifth Priority: Delegate to MCP
If a capability is already provided by an MCP server (like DeepWiki, Playwright), the Skill doesn’t need to install anything locally — just orchestrate MCP calls in SKILL.md. This essentially delegates all environment management to the MCP server maintainer.
This is also why I say Skills partially take on MCP’s role: for simple tools, writing a Skill script is much lighter than standing up an MCP server, but when functionality is complex enough to require an independent process and full dependency management, MCP remains the more appropriate vehicle.
Environment Decision Flowchart
Does this feature need external Python libraries?
├─ No → stdlib-only (zero environment cost, preferred)
└─ Yes
├─ Pure Python library, only 1-2 needed? → vendor into Skill directory
└─ Many dependencies or C extensions?
├─ Needs long-lived process? → per-skill venv + run.sh wrapper
└─ No → uv run + PEP 723 inline declarations
Part 5: Credentials and State Management
Three Forms of Credentials
Across my seven Skills, credential management takes three forms, each suited to different scenarios.
Environment variables work well for one-off API tokens. cloudflare-dns requires the user to set a CF_API_TOKEN environment variable, and the script reads directly from the environment. The benefit is that credentials don’t touch disk (not written to files), providing higher security. The downside is that users need to re-set them for each new shell session, unless they add them to their shell rc file.
Configuration files work well for multiple persistent credentials. hifi-download needs credentials for Spotify, Last.fm, Qobuz, and TIDAL — managing these with environment variables would be chaotic. In this scenario, a .env file with a .env.example template is a more sensible approach.
Browser login state works for OAuth-style authentication. tech-research’s Grok access requires X/Twitter login state — this isn’t an API key you can write to a config file, but a browser cookie session. What the Skill can do is detect login status, cache the detection result, and prompt the user to re-login when it expires.
Unified State Directory
All Skills’ persistent state (credential files, caches, configuration) should be placed under ~/.claude/<skill-name>/. Don’t invent your own paths — my early zlib-download put config in ~/.claude/book-tools/, which was both inconsistent and unintuitive. A unified path convention gives users a predictable location when they need to manually manage (clean, backup, migrate) state.
~/.claude/
├── hifi-download/
│ ├── .env # Credentials (Spotify, Qobuz, etc.)
│ └── .cache # OAuth token cache
├── zlib-download/
│ ├── .env # Credentials (Z-Library, Anna's Archive)
│ └── config.json # Runtime config (auto-generated remix tokens, etc.)
├── tech-research/
│ └── .grok-status.json # Login status cache
└── cloudflare-dns/ # (No persistent state, credentials via env vars)
The Login State Cache Design Pattern
tech-research’s Grok login status cache is a pattern worth promoting. Its core idea is optimistic assumption + passive invalidation:
By default, assume the user is logged in (optimistic). Only mark as logged_out when the agent detects a “Sign in” page during actual access. The logged_out state has a 2-hour expiry — after that, the system returns to optimistic mode because the user may have re-logged in.
This design avoids two poor alternatives: requiring the user to confirm login status before every use (annoying), or never trying again after a login failure (overly pessimistic).
Part 6: User-Facing Experience Tips
The first five sections systematically discussed how to design good tool interfaces for agents. But the end user of a Skill is a person — the agent is just the middle layer. Here are the key points I’ve accumulated that directly impact user experience:
Don’t ask for passwords in chat. The agent should never request API keys or passwords in conversation — chat logs may be saved, making this both insecure and inconvenient. Guide users to edit ~/.claude/<skill-name>/.env using their preferred editor.
Unify state directories. All Skills’ persistent state goes under ~/.claude/<skill-name>/. Don’t invent your own paths. Users should have a predictable location when cleaning up, backing up, or migrating.
Clearly separate automatic and manual setup. SKILL.md should explicitly indicate which steps the agent handles automatically (installing deps, creating directories) and which need user action (filling credentials, logging into services, launching desktop apps). Don’t make users guess.
Don’t freeze when services go down. Annotate each external dependency with its degradation mode (skip and report / automatic fallback / disable individual service / terminate and guide), so the agent can make reasonable decisions rather than retrying endlessly.
Make first runs fast. Your environment strategy choice directly impacts the user’s waiting experience when first triggering a Skill. stdlib-only means zero wait, uv means seconds, pip + venv could mean tens of seconds. Prefer lightweight solutions.
Use optimistic caching for login state. For browser login states that can’t be actively detected, use “optimistic assumption + passive invalidation”: default to logged-in, mark as expired only on actual access failure, set a cooldown before retrying. Don’t ask users to confirm every time, and don’t permanently give up after one failure.
Use background mode for long operations. Download progress panels, real-time monitoring dashboards, and other long-lived processes should be explicitly noted in SKILL.md to use background mode (Bash tool’s run_in_background: true). Otherwise the agent blocks synchronously — the user gets no “started” feedback and the agent can’t proceed with subsequent operations. hifi-download’s download UI is a prime example.
Tell users where results are. Downloaded a file? Report the path and size. Searched for resources? Report the entry count and remaining quota. Generated a config? Report the file location. Users shouldn’t have to dig through the filesystem to find results.
Part 7: Security
I haven’t done systematic research on this part, and frankly it’s not my area of expertise. But after surveying community discussions, I believe this topic needs to be raised — between 2025 and 2026, real security incidents have already emerged in the AI agent skill/plugin ecosystem (malicious skill poisoning, MCP server command injection, IDE plugins stealing crypto wallets), and OWASP has even published a dedicated Agentic Applications Top 10.
First, let’s set boundaries: We’re not discussing how to defend against malicious developers — poisoning, backdoors, malware disguised as legitimate skills. That’s the responsibility of platforms, registries, and supply chain security systems. Our focus is the other side: how a well-intentioned skill developer can avoid leaving security vulnerabilities through poor coding practices. Think of it like OWASP Secure Coding Practices in web development — not teaching you to stop hackers, but teaching you not to leave holes in your own code.
Here are the points I’ve seen from community practice that are most relevant to skill development:
Don’t concatenate user input directly into shell commands. This is classic command injection. Use Python’s subprocess.run(["cmd", arg]) list form, not shell=True with string concatenation. Similarly, validate path parameters to prevent ../../ traversal to unexpected directories.
Don’t include credentials in error messages. Printing full requests (including Authorization headers) in stderr is convenient during debugging, but this output enters the agent’s context and may be logged or echoed back to users. Sanitize before outputting.
Set reasonable permissions on .env files. chmod 600 ensures only the file owner can read and write. If a skill’s setup flow creates .env files, permissions should be set at creation time.
Don’t disable HTTPS certificate verification. urllib.request verifies certificates by default — don’t pass context=ssl._create_unverified_context() for “convenience.” If you encounter certificate issues, fix the certificate, don’t disable verification.
Don’t hardcode paths and secrets in scripts. Our validate.py already has automatic detection for this (hardcoded paths, OpenAI/GitHub/AWS/Slack API key patterns), but it’s worth emphasizing the awareness as well.
This field is evolving rapidly, and the above is just basic code hygiene. If you want to dive deeper into the full picture of AI agent security, these are worth reading:
- OWASP Top 10 for Agentic Applications — Agent-specific top 10 risk categories
- Anthropic: Sandboxing in Claude Code — Claude Code’s sandboxing mechanisms and security model
- Unit 42: MCP Attack Vectors — Attack surface analysis of the MCP protocol layer
- Snyk: ToxicSkills — Analysis of the first large-scale malicious skill poisoning incident
- Tencent Zhuque Lab: 2025 MCP Top 10 Vulnerabilities — MCP security research from the Chinese community
Part 8: Runtime Declarations in SKILL.md
All the practices discussed above ultimately need to be reflected in SKILL.md, so Claude knows how to interact with the Skill. I recommend adding a ## Runtime section to SKILL.md that declaratively describes runtime behavior.
## Runtime
### Preflight
Run `python3 scripts/main.py preflight` to get environment status.
Output is standard JSON: when `ready` is true, proceed directly; when false, guide the user based on `hint` fields.
### Setup (First Run)
**Automatic steps** (Claude executes):
1. Check if uv is installed; if not, prompt user to `brew install uv`
2. On first invocation, uv auto-installs dependencies — no extra action needed
**Manual steps** (guide user):
1. Copy .env.example to ~/.claude/<skill-name>/.env
2. Edit .env to fill in API credentials
Do not ask for passwords or API keys in chat — guide users to edit config files.
### Credentials
| Credential | Location | Required | How to obtain |
|-----------|----------|----------|---------------|
| Spotify | ~/.claude/hifi-download/.env | Yes | developer.spotify.com |
| Qobuz | ~/.claude/hifi-download/.env | No | qobuz.com membership |
### Degradation
| Dependency | Behavior when unavailable |
|-----------|--------------------------|
| Spotify | Cannot search or recommend; terminate and prompt to configure |
| Qobuz | Skip Qobuz download source, use TIDAL |
| TIDAL | Skip TIDAL download source, use Qobuz |
| Both Qobuz + TIDAL unavailable | Can search and recommend, but cannot download |
This declarative approach gives Claude clear decision criteria when triggering a Skill, rather than having to search for clues in long prose.
Conclusion
Back to the opening question: the official docs teach you how to write a SKILL.md, but don’t tell you how to design the experience after the Skill is running.
These best practices fill that gap — the first five parts cover how to design tool interfaces for agents (layering, scripts, lifecycle, environment, credentials), the sixth part covers UX tips, and the seventh covers basic security code hygiene (which I’m still learning about).
None of this is theoretical — it’s all distilled from iterating across over a dozen Skills of different types. They’re still evolving, but I believe that at the current stage of the Skill ecosystem, these directions are right.
