Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Tips for Building Useful Agent Skills

Tips for Building Useful Agent Skills

How to build skills for AI agents that actually work. I'll be covering how to define the right boundaries, provide useful context, and decide what belongs in a skill and what's not, and share couple of tips from my bag of tricks.

Referenced code: https://github.com/RStankov/talks-code/tree/master/2026.09.12%20-%20Tips%20for%20Building%20Useful%20Agent%20Skills

Avatar for Radoslav Stankov

Radoslav Stankov

September 07, 2026

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. ~/.claude/skills/[name-of-skill]/SKILL.md --name: rado-download-gif description: Download the GIF from a Giphy

    page, convert it to MP4, and save it to ~/Desktop. --# Download Giphy GIF as MP4 ... ... ... ... ... ... ... ... ... ...
  2. import { generateText, tool, stepCountIs } from 'ai'; import {

    z } from 'zod'; // STEP 1 — Load skills const skills = Object.fromEntries( await Promise.all( ( await readdir('skills') ).map(async (dir) => { const content = await readFile(`skills/${dir}/SKILL.md`, 'utf8'); const { name, description } = parseFrontmatter(content); return [name, { description, content }]; }), ), ); // STEP 2 — Inject skill index into context const SYSTEM_PROMPT = `${BASE_CONTEXT}\n` + '<skills>\n' + Object.entries(skills) .map( ([name, { description }]) => `<skill name="${name}">${description}</skill>`, ) .join('\n') + '\n</skills>'; // STEP 3 — Load full content on demand via tool call export async function chatMessage(message: string) {
  3. `<skill name="${name}">${description}</skill>`, ) .join('\n') + '\n</skills>'; // STEP 3 —

    Load full content on demand via tool call export async function chatMessage(message: string) { const result = await generateText({ model: openai('gpt-5'), system: SYSTEM_PROMPT, prompt: message, tools: { load_skill: tool({ description: `Load full instructions for a skill by name. Available: ${Object.keys( skills, ).join(', ')}`, inputSchema: z.object({ name: z.string() }), execute: ({ name }) => { if (!skills[name]) return `No skill named "${name}". Available: ${Object.keys( skills, ).join(', ')}`; return skills[name].content; }, }), // ... other tools }, stopWhen: stepCountIs(10), }); return result.text; }
  4. Skills should be readable by humans, but they are for

    agents to consume. Keep in mind that agents know a lot. Keep only the non-obvious parts.
  5. 0/ Read skill very carefully (look for hidden symbols) 1/

    Copy it with a prefix like "matt-grill-me" / "matt-grill-with-docs" 2/ Create CREDIT.md that links to the original 3/ Adjust to my preferences
  6. I should make skill out of this % 0/ Read

    skill very carefully (look for hidden symbols) 1/ Copy it with a prefix like "matt-grill-me" / "matt-grill-with-docs" 2/ Create CREDIT.md that links to the original 3/ Adjust to my preferences
  7. 0/ Read skill very carefully (look for hidden symbols) 1/

    Copy it with a prefix like "matt-grill-me" / "matt-grill-with-docs" 2/ Create CREDIT.md that links to the original 3/ Adjust to my preferences
  8. $ TIP 2 Prefix your skills. I use: - `rado-`

    for my personal skills - `doczen-` for company skills
  9. $ TIP 3 Repo-specific skills related to common operations, like

    opening PRs in a certain format or coding rules not covered by linters.
  10. --name: rado-fix-pr-comments description: Fetch unresolved GitHub pull-request review comments, present

    them one by one, and fix the ones the user approves. disable-model-invocation: true --# Fix Pull Request Review Comments Fetch all review comments on the current branch's PR and walk through them interactively. ## Steps 1. **Get the PR number** for the current branch: ``` gh pr view --json number -q '.number' ``` 2. **Fetch unresolved review threads** using the GraphQL API (the REST API does not expose resolved status): ``` gh api graphql -f query=' query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviewThreads(first: 100) { nodes { isResolved
  11. 2. **Fetch unresolved review threads** using the GraphQL API (the

    REST API does not expose resolved status): ``` gh api graphql -f query=' query($owner: String!, $repo: String!, $pr: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $pr) { reviewThreads(first: 100) { nodes { isResolved comments(first: 10) { nodes { path line originalLine author { login } body } } } } } } } ' -f owner=OWNER -f repo=REPO -F pr=PR_NUMBER \ --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[0] | "---", "File: " + .path + ":" + ((.line // .originalLine) | tostring), "Author: " + .author.login, "Body: " + .body, ""' ```
  12. body } } } } } } } ' -f

    owner=OWNER -f repo=REPO -F pr=PR_NUMBER \ --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[0] | "---", "File: " + .path + ":" + ((.line // .originalLine) | tostring), "Author: " + .author.login, "Body: " + .body, ""' ``` 3. **Present each comment one at a time**. For each comment show: - File and line number - Author - The comment body - The current code at that location - Your suggested fix (if you have one) 4. **Wait for user input** before proceeding. The user will either: - Approve the fix (apply it) - Give alternative instructions on how to fix - Skip the comment 5. After going through all comments, **verify** fixes compile/pass lint by running the relevant quality check. 6. **Do NOT commit or push** unless explicitly asked.
  13. --name: rado-code-review-pr description: Review a GitHub pull request in an

    isolated git worktree, without touching the current working tree. Accepts a PR URL or number. disable-model-invocation: true --# Code Review PR Review a GitHub pull request without modifying your current working tree. Fetches the PR into a temporary `git worktree`, runs the full code review from the `rado-code-review` skill, then cleans up. ## Guidance - When posting PR review findings to GitHub, always anchor comments to specific diff lines as an inline review — never post as top-level/bundled comments. - Don't use over-long sentences, explain issues simply. ## Steps ### Step 1 — Fetch the PR **Parse the input.** Accept either a full GitHub PR URL (`https://github.com/owner/repo/pull/ 123`) or a bare PR number. If no input is provided, use the PR for the current branch. **Fetch PR metadata:** ```bash gh pr view <url_or_number> --json number,headRefName,baseRefName,title,body,url ```
  14. ### Step 2 — Run the code review Get the

    diff against the base branch: ```bash git diff origin/$BASE...pr-$NUMBER-review git log origin/$BASE...pr-$NUMBER-review --oneline ``` Run the `rado-code-review` skill and follow its steps — including spawning its review and double-check subagents — and OUTPUT REVIEW EXACTLY as its `Step 4 - Output`. The base branch for diffing is `origin/<baseRefName>`, not `main`. Include the PR title and description as context when evaluating intent and completeness. ### Step 3 — Interactive Q&A After delivering the review, stay in context. The diff and PR metadata are loaded — answer follow-up questions about the PR without re-fetching. To read a specific file from the PR branch in depth: ```bash git show pr-$NUMBER-review:path/to/file ``` ### Step 4 — Clean up Only clean up when the user explicitly says they are done with the PR. Do not clean up automatically after delivering the review — they may want to explore further.
  15. --name: rado-code-review-pr-comment description: Turn code review findings into inline PR

    comments written in my voice, one at a time, for me to review and post myself. disable-model-invocation: true --# Code Review PR Comment Takes code review findings that are already in context and drafts them as inline PR comments, one at a time, so I can review each one before it goes out. ## Never post I post the comments myself. Do not run `gh api`, `gh pr comment`, `gh pr review`, or anything else that writes to GitHub. Only post if I explicitly ask you to in a later message. ## Voice Every comment goes out under my name, so it has to sound like me. Read `STYLE.md` in this skill directory before drafting the first comment and follow it for every comment, including rewrites. ## Steps ### Step 1 - Collect the findings Use the review findings already in context, wherever they came from. If none are loaded, ask which review to draft.
  16. ## Steps ### Step 1 - Collect the findings Use

    the review findings already in context, wherever they came from. If none are loaded, ask which review to draft. If I asked for a subset, filter to those. Otherwise take all of them. Count them, that count is the denominator. ### Step 2 - One comment at a time Show one comment per turn. Never dump the whole batch. Each turn shows: ``` Comment: 2 / 12 app/models/user.rb:42 ``` Then show me the code snippet this comment is for, so I can understand the comment better and the comment body exactly as it would appear on GitHub. Before showing a comment, check it against `STYLE.md`: - The marker is earned. & only if it blocks the merge, ✂ only for code that goes away, no marker otherwise. - The `suggestion` block applies cleanly to the lines it is anchored to. - Every claim about behavior was traced in the code. If it wasn't, it is phrased as a
  17. the comment body exactly as it would appear on GitHub.

    Before showing a comment, check it against `STYLE.md`: - The marker is earned. & only if it blocks the merge, ✂ only for code that goes away, no marker otherwise. - The `suggestion` block applies cleanly to the lines it is anchored to. - Every claim about behavior was traced in the code. If it wasn't, it is phrased as a question. - One to three sentences, unless it is a refactor proposal with numbered steps. - Nothing from the "What to avoid" list is in it. A comment that fails a check gets fixed before I see it. Don't show it with an explanation of what's wrong with it. Then stop and wait. I will either accept it, ask a question, tell you to rewrite it, or drop it. Answer questions and rewrite in place, re-showing the comment with the same counter, until I accept or drop it. Move to the next one only when the current one is settled. ### Step 3 - Hand off The PR is done when every comment has been accepted or dropped. Then print the accepted comments as one list, each with its `file:line` and body, ready for me to paste into GitHub. Note anything I dropped and anything that landed outside the diff, with its real location. ### Step 4 - Clean up PR Consider PR to be done and clean it up.
  18. --name: rado-vibe-html-app description: disable-model-invocation: true --# Vibe HTML app Use

    a single HTML file with inlined JavaScript and CSS. No external, don't use React or Tailwind. Keep code concise and simple. Persist state via URL or localStorage when relevant. Everything must run entirely in the browser. # Additional files - Generate `features.txt`, where store Gherkin Given/When/Then format all features from the user point of view build in this app. Keep up to date. - Generate CLAUDE.md, to be used in the future. Reference current instructions.
  19. Ask Claude to improve existing skills. Ask Claude why the

    skill was not triggered. Ask Claude to scan your codebase for coding style. Ask Claude to scan your sessions for new skills.
  20. Ask Claude to improve existing skills. Ask Claude why the

    skill was not triggered. Ask Claude to scan your codebase for coding style. Ask Claude to scan your sessions for new skills.
  21. --name: rado-database-structure-formatting description: disable-model-invocation: true --# Database Structure Formatting Present

    every table, record, and schema proposal in this format for the rest of the conversation. ``` [table] [column] [column] - [description] [column] - [description] (enum value, enum value) ``` ## Rules - Exclude `id`, `created_at`, `updated_at`. - Description is optional — omit it when the column name says everything. - `[name]_id` drops the suffix: `author - record, who wrote it` - Polymorphic `[name]_type` + `[name]_id` collapse to one line: `target - polymorphic record, [description] (Post, Comment)` - Enums list their values in parentheses at the end of the line. - Columns only — no `has_many` or other reverse associations. This format describes the *shape* of the design. No column types, defaults, nullability, constraints, or indexes — don't raise them unless asked.
  22. ``` ## Rules - Exclude `id`, `created_at`, `updated_at`. - Description

    is optional — omit it when the column name says everything. - `[name]_id` drops the suffix: `author - record, who wrote it` - Polymorphic `[name]_type` + `[name]_id` collapse to one line: `target - polymorphic record, [description] (Post, Comment)` - Enums list their values in parentheses at the end of the line. - Columns only — no `has_many` or other reverse associations. This format describes the *shape* of the design. No column types, defaults, nullability, constraints, or indexes — don't raise them unless asked. ## Example ``` posts author - record, who wrote it title body - markdown source status - publication state (draft, scheduled, published, archived) published_at - when status became published, blank until then reactions user - record, who reacted target - polymorphic record, what was reacted to (Post, Comment, Photo) kind (like, celebrate, insightful) ```
  23. $ TIP 9 You need to re-evaluate skills with each

    model generation. Some skills might work well with one model and break with another. Keep skill simple and small.
  24. ( Recap 0. Know the basics 1. disable-model-invocation: true 2.

    Prefix your skills 3. Repo/company specific skills 4. Symlink skills folder 5. Build interactive skills 6. Chain of skills 7. Record keeping 8. Improvement loops 9. Re-evaluate with new models