Python SDK¶
Tldr
pip install agr and use Skill.from_git("owner/repo/skill") to load
skills programmatically. Discover skills with list_skills(), manage the
cache with cache.clear(), and handle errors via AgrError subclasses.
Prerequisites: Python 3.10+
Use agr as a Python library to load, inspect, and cache skills programmatically.
What is a skill? A folder containing a SKILL.md file with YAML frontmatter
(name, description) and markdown instructions for an AI coding agent. Skills
work across Claude Code, Cursor, Codex, OpenCode, GitHub Copilot, and
Antigravity. A handle like "anthropics/skills/code-review" points to a
skill directory inside a GitHub repo.
Install the agr package¶
Tip
If you want the agr and agrx CLI tools (not just the SDK), install with uv tool install agr or pipx install agr instead. See the Tutorial for a full walkthrough.
Load a skill in 3 lines¶
from agr import Skill
# Load a skill from GitHub
skill = Skill.from_git("anthropics/skills/code-review")
print(skill.prompt) # Contents of SKILL.md
print(skill.files) # List of files in the skill directory
# Load a local skill
skill = Skill.from_local("./my-skill")
print(skill.prompt)
Load skills from GitHub or local paths¶
From GitHub¶
Skill.from_git() downloads a skill from GitHub and caches it locally using a
handle to identify the skill. On subsequent calls, agr
checks the remote HEAD commit — if the cached revision matches, it returns the
cached copy without re-downloading.
from agr import Skill
# Short handle (looks in user's "skills" repo)
skill = Skill.from_git("kasperjunge/commit")
# Explicit repo
skill = Skill.from_git("anthropics/skills/code-review")
# Force re-download even if cached (useful after upstream changes)
skill = Skill.from_git("kasperjunge/commit", force_download=True)
# Private repos — set GITHUB_TOKEN or GH_TOKEN in your environment
# export GITHUB_TOKEN="ghp_..."
skill = Skill.from_git("my-org/private-repo/internal-skill")
Private repositories
from_git() uses the same GITHUB_TOKEN / GH_TOKEN environment
variables as the CLI. See Private Repositories
for token setup.
From a Local Directory¶
Skill.from_local() loads a skill from a local path. The directory must contain a SKILL.md file (see Creating Skills for the expected structure).
Skill properties and metadata¶
| Property | Type | Description |
|---|---|---|
name |
str |
Skill name (directory name) |
path |
Path |
Path to skill directory (cached or local) |
handle |
ParsedHandle \| None |
Parsed handle with owner, repo, and name components |
source |
str \| None |
Source name the skill was fetched from (e.g., "github") |
revision |
str \| None |
Git commit hash (first 12 chars) of the fetched revision |
prompt |
str |
Contents of SKILL.md (lazy-loaded on first access) |
files |
list[str] |
Relative file paths in the skill directory (lazy-loaded on first access) |
metadata |
dict |
Combined metadata dict (see below) |
content_hash |
str \| None |
Content hash from .agr.json, if present |
The handle, source, and revision attributes are set by from_git(). For
locally loaded skills, source and revision are None.
Provenance: handle, source, revision¶
When you load a skill from GitHub, agr records where it came from:
skill = Skill.from_git("anthropics/skills/code-review")
# Which repo was it fetched from?
print(skill.source) # "github"
print(skill.revision) # "a1b2c3d4e5f6" (short commit hash)
# Access handle components
print(skill.handle.username) # "anthropics"
print(skill.handle.repo) # "skills"
print(skill.handle.name) # "code-review"
The metadata dict¶
The metadata property returns a dict combining all provenance info:
skill = Skill.from_git("anthropics/skills/code-review")
print(skill.metadata)
# {
# "name": "code-review",
# "path": "/Users/you/.cache/agr/skills/anthropics/skills/code-review/a1b2c3d4e5f6",
# "source": "github",
# "revision": "a1b2c3d4e5f6",
# "handle": "anthropics/skills/code-review",
# "is_local": False,
# }
Reading Files¶
skill = Skill.from_git("anthropics/skills/code-review")
# Read any file in the skill directory
content = skill.read_file("scripts/lint.sh")
Path traversal is blocked — paths containing .. or resolving outside the skill directory raise ValueError.
Recomputing Content Hash¶
# Compare stored hash with current files on disk
stored = skill.content_hash
current = skill.recompute_content_hash()
if stored != current:
print("Skill files have changed")
Discover skills in a repository¶
List all skills in a repo¶
from agr import list_skills
# List all skills in a repo
skills = list_skills("anthropics/skills")
for info in skills:
print(f"{info.handle}: {info.name}")
# List skills in a user's default "skills" repo
skills = list_skills("kasperjunge")
list_skills() does not fetch descriptions
list_skills() discovers skills from the repository tree without downloading
each SKILL.md. The description field on returned SkillInfo objects is
None. To get the description for a specific skill, use skill_info().
Get details for a single skill¶
skill_info() fetches the skill's SKILL.md to extract its description (first
body paragraph after frontmatter, up to 200 chars):
from agr import skill_info
info = skill_info("anthropics/skills/code-review")
print(info.name) # "code-review"
print(info.handle) # "anthropics/skills/code-review"
print(info.description) # First body paragraph from SKILL.md
print(info.owner) # "anthropics"
print(info.repo) # "skills"
Both functions use the GitHub API and respect GITHUB_TOKEN / GH_TOKEN environment variables for authentication. See Troubleshooting if you hit rate limits or auth errors.
Manage the download cache¶
Downloaded skills are cached in ~/.cache/agr/skills/ (also used by the CLI). The cache object provides inspection and cleanup.
from agr import cache
# Cache location
print(cache.path) # ~/.cache/agr
# Cache statistics
info = cache.info()
print(info["skills_count"]) # Number of cached skills
print(info["size_bytes"]) # Total size in bytes
# Clear all cached skills
deleted = cache.clear()
# Clear specific skills (glob pattern)
deleted = cache.clear("anthropics/skills/*")
deleted = cache.clear("kasperjunge/*/*")
Handle errors with AgrError subclasses¶
All SDK errors inherit from AgrError, including network failures. Catch
specific subclasses for targeted handling, or catch AgrError as a fallback
for any SDK error (including network issues like DNS failures and timeouts):
from agr import Skill, list_skills, skill_info
from agr.exceptions import (
AgrError,
InvalidHandleError,
InvalidLocalPathError,
SkillNotFoundError,
RepoNotFoundError,
AuthenticationError,
RateLimitError,
CacheError,
)
try:
skill = Skill.from_git("nonexistent/skill")
except SkillNotFoundError:
print("Skill not found in repository")
except RepoNotFoundError:
print("Repository does not exist")
except AuthenticationError:
print("Set GITHUB_TOKEN for private repos")
except RateLimitError:
print("GitHub API rate limit exceeded")
except AgrError as e:
print(f"Unexpected error: {e}") # Network failures, etc.
try:
skill = Skill.from_local("./missing-skill")
except InvalidLocalPathError:
print("Path does not exist or is missing SKILL.md")
try:
skills = list_skills("not a valid handle/a/b")
except InvalidHandleError:
print("Bad repo handle format")
try:
info = skill_info("owner/nonexistent-skill")
except SkillNotFoundError:
print("Skill not found in that repo")
Network errors are AgrError, not ConnectionError
Network failures (DNS resolution, timeouts, connection refused) in
list_skills(), skill_info(), and Skill.from_git() raise AgrError
— not Python's built-in ConnectionError. If your code catches AgrError
(or its subclasses), network errors are included automatically.
Type definitions¶
ParsedHandle¶
Returned by skill.handle:
@dataclass
class ParsedHandle:
username: str | None # GitHub username (None for local skills)
repo: str | None # Repository name (None = default "skills" repo)
name: str # Skill name (final segment of the handle)
is_local: bool # True for local path references
local_path: Path | None # Original local path (if is_local)
ParsedHandle also has an is_remote property that returns True for GitHub references.
SkillInfo¶
Returned by list_skills() and skill_info():
@dataclass
class SkillInfo:
name: str # Skill name
handle: str # Full handle (e.g., "owner/repo/skill")
description: str | None # First body paragraph from SKILL.md (None from list_skills, populated from skill_info)
repo: str # Repository name
owner: str # GitHub owner/username
Next Steps¶
- Creating Skills — build your own skills to load with the SDK
- Core Concepts — understand handles, sources, and the sync lifecycle
- CLI Reference — manage skills from the command line instead of Python