SkillKit

An Elixir library for building LLM agent systems. SkillKit provides an application runtime where agents, skills, hooks, and subagents are defined in markdown files and composed at startup — no framework scaffolding, no code generation.

SkillKit's skill format is compatible with the Agent Skills open standard and aligned with the Claude Code plugin structure. Skills you write for SkillKit work in Claude Code, and vice versa.

Quick Start

Add SkillKit to your dependencies:

# mix.exs
{:skill_kit, "~> 0.1.0"}

Set your API key and start chatting:

export ANTHROPIC_API_KEY=sk-ant-...
# Interactive chat with a sample agent
mix skill_kit.chat neve
# Single prompt
mix skill_kit.demo "What is 2 + 2?"

Or use the API directly:

# Point at a directory containing an AGENT.md
{:ok, agent} = SkillKit.start_agent("agents/neve",
skills: ["skills", SkillKit.Tools.Shell],
caller: self()
)
# Send a message
:ok = SkillKit.send_message(agent, "Review lib/skill_kit.ex")
# Receive streamed events
receive do
%SkillKit.Event.Delta{text: text} -> IO.write(text)
%SkillKit.Types.AssistantMessage{} -> IO.puts("\nDone.")
%SkillKit.Event.Error{reason: reason} -> IO.puts("Error: #{inspect(reason)}")
end
# Stop
SkillKit.stop_agent(agent)

Core Concepts

Agents

Agents are LLM-powered OTP processes defined in AGENT.md files with YAML frontmatter:

---
name: "neve"
description: "A helpful coding assistant"
model: "claude-sonnet-4-20250514"
metadata:
max_agent_depth: 2
---
Your name is Neve. You are a helpful coding assistant.

Each agent starts its own supervision tree — Registry, Catalog, Mailbox, Server, and SubagentSupervisor — fully isolated from other agents.

Skills

Skills are markdown files that inject instructions into an agent's context. The standard layout (from the Agent Skills spec and Claude Code plugins) is a SKILL.md inside a named directory:

---
name: "system:memory"
description: "Persistent memory management"
---
You have persistent memory stored in `.memory/current.md`.
At the START of every conversation, read your memory file...

Skills support template tokens ($ARGUMENTS, $SKILL_DIR, $SESSION_ID), scope variable resolution ($USERNAME, $TENANT), and dynamic command injection (!`git branch --show-current`) that runs at render time.

Hooks

Skills can define hooks that fire at agent boundaries — before and after tool use, subagent delegation, LLM requests, and more:

hooks:
PreToolUse:
- matcher: "bash"
hooks:
- type: command
command: "check-policy $TOOL_NAME"
PostToolUse:
- matcher: ".*"
hooks:
- type: http
url: "https://audit.example.com/log"

Hooks are gate-only: they can allow, deny, or suspend a boundary crossing, but they cannot transform data. Pre-hooks run synchronously and block the action. Post-hooks fire asynchronously and their return values are ignored.

Subagents

Agents delegate work to child agents asynchronously. The parent invokes a subagent as a tool call, continues its own work, and receives the result when the child finishes:

---
name: "code-reviewer"
description: "Reviews code for issues and reports findings"
---
You are a code reviewer. Use bash to read files, analyze them,
then call report_result with your findings.

Delegation depth is enforced via max_agent_depth in the agent definition.

Authorization

Scope-based access control restricts which skills a caller may discover and activate. Skills declare required_scope in their frontmatter; callers provide granted scopes via a struct implementing SkillKit.Scope.

Loading Kits

start_agent/2 takes an agent source as its first argument and a skills: option listing additional kits. Both accept three forms:

FormResolves toExample
"path" (string){SkillKit.Kit.Local, dir: "path"}"skills" loads skills/ directory
Module (bare atom){Module, []}SkillKit.Tools.Shell adds bash execution
{Module, opts} (tuple)Used as-is{SkillKit.Kit.Local, dir: "/abs/path"}

The agent's own kit is auto-included in the tool pool. When you pass "agents/neve" as the agent, its AGENT.md defines the identity (name, model, system prompt) and any skills or subagents in that directory become available tools — no need to list it again in skills:.

# "agents/neve" provides the agent identity + its own skills.
# "skills" adds a shared skills directory.
# SkillKit.Tools.Shell adds bash tool execution.
SkillKit.start_agent("agents/neve",
skills: ["skills", SkillKit.Tools.Shell],
scope: my_scope,
conversation_store: {SkillKit.Conversation.Store.Filesystem, path: ".conversations"}
)

Module-backed kits (use SkillKit.Kit) work the same way — they implement both the Kit.Provider behaviour (to load skills from a co-located skills/ directory) and Tool (to execute them). See the Providers guide for details.

Configuration

# config/config.exs
config :skill_kit, SkillKit.LLM,
providers: [
anthropic: SkillKit.LLM.Anthropic
],
default_provider: :anthropic

Credentials

SkillKit.Tools.Shell runs commands in a hermetic child environment (env -i) so arbitrary BEAM env vars do not leak into LLM-driven shell sessions. To expose secrets to the shell, implement a SkillKit.CredentialProvider and configure it:

# config/config.exs
config :skill_kit, credential_provider: MyApp.Credentials
# lib/my_app/credentials.ex
defmodule MyApp.Credentials do
@behaviour SkillKit.CredentialProvider
@allowlist %{
"GITHUB_TOKEN" => "GITHUB_PAT",
"NPM_TOKEN" => "NPM_PUBLISH_TOKEN"
}
@impl true
def list(SkillKit.Tools.Shell, _agent), do: Map.keys(@allowlist)
def list(_tool, _agent), do: []
@impl true
def fetch(SkillKit.Tools.Shell, _agent, key) do
case Map.fetch(@allowlist, key) do
{:ok, env_var} -> {:ok, System.get_env(env_var)}
:error -> {:ok, nil}
end
end
def fetch(_tool, _agent, _key), do: {:ok, nil}
end

The agent struct is passed to every call so implementations can gate access on agent.scope or any other field. Return {:ok, nil} to deny a key cleanly, or :error to signal provider failure. Tools.Shell drops any key that doesn't return {:ok, value} from the child's environment.

Every fetch/3 call emits a [:skill_kit, :credential, :fetch] telemetry event with key, tool, agent, scope, and outcome metadata — values are never included. Attach a handler for audit logging.

Without a configured provider, the SkillKit.CredentialProvider module itself acts as a null provider — Tools.Shell runs with only PATH and HOME in the child environment, no credentials.

Telemetry

SkillKit emits :telemetry events for observability and cost tracking:

EventMeasurementsNotes
[:skill_kit, :turn, :start/:stop]system_time / durationOne agent loop (batch of messages)
[:skill_kit, :llm_request, :start/:stop]system_time / durationLLM call inside a turn
[:skill_kit, :tool_use, :start/:stop]system_time / durationTool or module-skill execution
[:skill_kit, :subagent, :start/:stop]system_time / durationSpawning a subagent
[:skill_kit, :skill_activation, :start/:stop]system_time / durationActivating a skill
[:skill_kit, :conversation_save, :start/:stop]system_time / durationPersisting conversation
[:skill_kit, :conversation_load, :start/:stop]system_time / durationLoading conversation
[:skill_kit, :agent, :start/:stop]system_time / durationAgent process lifecycle
[:skill_kit, :llm, :stream, :start/:stop]system_time / durationRaw LLM HTTP stream
[:skill_kit, :llm, :stream, :error]Model URI could not be resolved
[:skill_kit, :agent, :orphaned_result]Subagent result with no parent
[:anthropic, :rate_limited]retry_after, attempt429 triggered automatic retry

See the Telemetry guide for handler examples and testing helpers.

Architecture

SkillKit.start_agent/2
|-> Agent (Supervisor, one_for_one)
|-> Registry (process discovery)
|-> Catalog (provider aggregation, authorization, tool definitions)
|-> Core (rest_for_one)
|-> Mailbox (message buffering)
|-> Server (LLM loop, tool execution, streaming)
|-> SubagentSupervisor (DynamicSupervisor)

Events flow: User -> send_message -> Mailbox -> Server -> LLM -> stream deltas to caller -> execute tools -> loop until done -> send AssistantMessage.

See the Architecture guide for the full supervision tree, message flow, and module boundaries.

Webhooks

Agents can register HTTP webhook endpoints through the SkillKit.Tools.Webhook kit. The adapter ships a Plug for the host to mount, a Registry that tracks running agents by name, and vendor verifier modules for GitHub, Stripe, and Slack.

Host wiring:

# Application tree:
children = [{SkillKit.Webhook, []}]
# Phoenix/Plug router:
forward "/webhooks", to: SkillKit.Webhook.Plug
# Per agent:
SkillKit.start_agent("agents/support",
skills: [{SkillKit.Tools.Webhook,
verifiers: %{
"stripe" => SkillKit.Webhook.Verifier.Stripe,
"github" => SkillKit.Webhook.Verifier.Github,
"slack" => SkillKit.Webhook.Verifier.Slack
}}])

Also add a Plug.Parsers body-reader so HMAC can verify the raw bytes:

plug Plug.Parsers,
parsers: [:json, :urlencoded],
body_reader: {SkillKit.Webhook.BodyReader, :read_body, []},
json_decoder: Jason

See docs/superpowers/specs/2026-04-21-webhook-adapter-design.md for the full design.

Guides

Standards Compatibility

SkillKit's skill format is compatible with:

License

MIT