Agent skills
This topic explains how to use agent skills with LaunchDarkly AI SDKs. This feature is available for AI SDKs only.
An agent skill is a single versioned SKILL.md document, with YAML front matter plus a Markdown body, that you manage in LaunchDarkly. Skills live in a project-scoped library, and you can attach them to an AgentControl config variation. LaunchDarkly AI SDKs let your application do various things with a skill:
- Discover which skills a config variation references after LaunchDarkly resolves it.
- Retrieve a skill’s content after LaunchDarkly verifies it.
- Materialize skills as files, which means agent runtimes, such as the Claude Agent SDK, can read them directly from disk.
How agent skills work
Keep the following in mind as you build with agent skills:
- A skill reference pins a version: A config variation names a skill reference in the form of a
{ key, version }pair. The SDK resolves the pinned version. A referenced skill has no “latest” version. - Skills have no targeting of their own: The skill a context receives is decided entirely by which config variation LaunchDarkly serves that context. Content accessors do not take a context argument.
- Content is opaque bytes: A skill’s content is
bytesin Python andUint8Arrayin Node.js. The SDK never parses it and does not expose a frontmatter accessor. If you need the front matter, decode and parse it manually. A skill’snameanddescriptioncome from metadata that LaunchDarkly stores, not from the content. This means they are available without decoding anything. - Integrity is verified before anything is returned: Every skill’s SHA-256 hash is checked against the hash LaunchDarkly delivered. Content that does not verify is withheld and is never returned or written to disk.
- *Skills are server-side only: Skill content is customer-confidential agent instruction text. Mobile keys and client-side IDs are rejected.
Prerequisites
To use agent skills, you need:
- A LaunchDarkly project with agent skills enabled and at least one skill created. To learn more, read Agent skills.
- The environment’s server-side SDK key.
- Python 3.12 or later for the Python AI SDK, or Node.js 20 or above for the Node.js (server-side) AI SDK.
- An agent runtime or provider integration that consumes skills, matching one of the patterns described in the Agent skill integration patterns section.
Install the SDK
Agent skills ships inside the existing AI SDK server packages. You do not install a separate package:
Agent skill integration patterns
The pattern you need decides which part of the API you use.
Provider-uploaded references do not need a skill store. skill_refs/skillRefs works on any resolved config, including a config evaluated with a client you supply yourself. If that is the only pattern you need, skip to Discover references on a resolved config.
AI SDKs
This feature is available for the following AI SDKs:
Python AI
Expand Python AI SDK code sample
Set up delivery
Filesystem and dynamic-loader patterns need skill content, which arrives through a skill store that you create and pass to init_client. The production store is FDv2SkillStore. Start it, wait for the first payload, then initialize the client with it:
Check the return value of wait_for_skills. It is false when the wait times out, when the store is closed, and when delivery has failed fatally. A fatal error, such as an unauthorized key, resolves wait_for_skills immediately rather than at the timeout. This means a boot gated on it does not proceed against a dead store. Read store.failed to distinguish between the SDK giving up and the SDK timing out.
During an outage, the store keeps serving the last content it received. Retrieval never blocks on the network. Calling get_skill immediately after start() can see an empty store, which is why wait_for_skills exists.
close() is final. A closed store still answers from content it already received, but delivery cannot be restarted. Construct a new store to resume.
Use an in-memory store for local development and tests
InMemorySkillStore is a dict-backed store for local development, tests, and bring-your-own-content. Put wire-shaped objects into it, including a correct contentHash (SHA-256, lowercase hex, over the verbatim UTF-8 bytes). Content whose hash does not match is withheld.
As an example, this constructs a store and adds a skill to it:
Discover references on a resolved config
skill_refs is a pure projection of the config the SDK already returns from inspect_config. It performs no input/output (I/O) and needs no store or client.
For example, this calls skill_refs on the resolved config to get its references:
To pass references through to a provider, map each (key, version) pair to the provider’s own skill ID:
Retrieve content
get_skill returns None rather than raising when a skill is unavailable for any reason. The function only raises an exception if no store is configured. get_skills accepts references or bare key strings, where a string means newest, and omits any entry that is missing or unverifiable.
Use get_skill_result when you need to distinguish an absent skill from a tampered one:
To learn what each reason means, read Distinguish an absent skill from a tampered one.
Materialize skills onto disk
The documented default is the config-scoped form. Materialize only what the resolved variation references into a directory your agent runtime reads. LaunchDarkly calls this directory the managed root. The SDK owns the files beneath it that its manifest lists, and only ever writes or deletes paths the manifest already tracks.
Here’s an example that materializes the resolved skills to disk:
write_skills is async for parity with the rest of the SDK, but it performs synchronous filesystem I/O. Wrap it in asyncio.to_thread if that matters to your application, and reconcile one root at a time.
To keep the managed root reconciled as skills change, use watch_skills. It runs write_skills once, then re-runs it whenever delivery changes. With a streaming store, a revoked skill, such as one removed from a variation or deleted, has its SKILL.md pruned from the managed root within seconds instead of at the next restart.
The following example keeps the root synced with a watcher:
Use one watcher per root, and do not call write_skills by hand against a root that has a watcher. To learn what write_skills does and does not do, read Materialize skills safely.
Node.js (server-side) AI
Expand Node.js (server-side) AI SDK code sample
Set up delivery
Filesystem and dynamic-loader patterns need skill content, which arrives through a skill store that you create and pass to initClient. The production store is FDv2SkillStore. Start it, wait for the first payload, then initialize the client with it:
Check the return value of waitForSkills. It is false when the wait timed out, when the store was closed, and when delivery has failed fatally. A fatal error, such as an unauthorized key, resolves waitForSkills immediately rather than at the timeout. This means a boot gated on it does not proceed against a dead store. Read store.failed to distinguish between the SDK giving up and the SDK timing out.
During an outage, the store keeps serving the last content it received. Retrieval never blocks on the network. Calling getSkill immediately after start() can see an empty store, which is why waitForSkills exists.
close() is final. A closed store still answers from content it already received, but delivery cannot be restarted. Construct a new store to resume.
Use an in-memory store for local development and tests
InMemorySkillStore is a dev/test store with a put() method. Put wire-shaped objects into it, including a correct contentHash (SHA-256, lowercase hex, over the verbatim UTF-8 bytes). Content whose hash does not match is withheld.
As an example, this constructs a store and adds a skill to it:
Discover references on a resolved config
skillRefs is a pure projection of the config the SDK already returns from inspectConfig. It performs no I/O and needs no store or client.
For example, this calls skillRefs on the resolved config to get its references:
To pass references through to a provider, map each (key, version) pair to the provider’s own skill ID, the same way you would in the Python AI SDK.
Retrieve content
getSkill returns null rather than throwing when a skill is unavailable for any reason. The function only throws an exception if no store is configured. getSkills accepts references or bare key strings, where a string means newest, and omits any entry that is missing or unverifiable.
Use getSkillResult when you need to distinguish an absent skill from a tampered one. To learn what each reason means, read Distinguish an absent skill from a tampered one.
Materialize skills onto disk
The documented default is the config-scoped form. Materialize only what the resolved variation references into the managed root that your agent runtime reads.
Here’s an example that materializes the resolved skills to disk:
To keep the managed root reconciled as skills change, use watchSkills. It runs writeSkills once, then re-runs it whenever delivery changes. With a streaming store, a revoked skill’s SKILL.md is pruned from the managed root within seconds instead of at the next restart.
The following example keeps the root synced with a watcher:
Use one watcher per root, and do not call writeSkills by hand against a root that has a watcher. To learn what writeSkills does and does not do, read Materialize skills safely.
Store options
The skill store accepts the following options to control delivery mode, endpoints, and retry behavior. Each option has a different name in Python and Node.js:
Python takes seconds for store options and Node.js takes milliseconds. The same is true of the watch_skills/watchSkills debounce. debounce takes 0.5s in Python, and debounceMs takes 500ms in Node.js.
The timeout option on write_skills/writeSkills is the exception. It is in seconds in both languages, and in Node.js it sits in the same options object as debounceMs.
Materialize skills safely
The following explains what materializing does and does not do:
- Layout is
<root>/<skill-key>/SKILL.md, matching the agentskills.io convention. This means you can point the root straight at.claude/skills/or an equivalent directory. - Materializing is a reconciliation.
write_skills/writeSkillsrecords what it owns in a manifest at<root>/.launchdarkly-skills.jsonand only ever overwrites or deletes paths that the manifest lists. If you place a file yourself at a managed path, it’s reported as anerrorand left untouched. If you do not commit materialized skills, add the manifest to.gitignore. - A byte-identical file is adopted. If an unmanaged file’s bytes already exactly match the content LaunchDarkly resolved, it is adopted into the manifest and reported
skipped_current. This is what makes a reconcile recoverable after a process is killed between writing a skill file and rewriting the manifest. Bytes that differ in any way are still refused. - LaunchDarkly content takes precedence at execution time. A managed file whose bytes differ from the resolved content, whether from a stale version or a local edit, is overwritten and reported
updated. - The root’s parent must exist. The call creates the leaf root directory only. A missing parent, a root that is a file, or a root that is a symlink is a caller error, distinct from a per-skill
erroraction. - A requested skill that was never delivered is an error. In that case,
report.okisfalse. A pinned reference resolving to nothing is not a silent success. - A store with no first payload does not prune. The run reports the retrieval as unavailable and leaves disk untouched, rather than reading an empty store as a full revocation.
"*"materializes the entire project library. This is an explicit opt-in that puts every skill’s description into the agent’s context, including skills no config references. Present the config-scoped form as the default, and"*"as a choice.
Options
The write_skills/writeSkills function accepts the following options to control pruning, timeout, and how it behaves when retrieval is unavailable:
Distinguish an absent skill from a tampered one
get_skill_result/getSkillResult runs the same retrieval as get_skill/getSkill, but reports why instead of collapsing to None/null. Use it to fail closed on suspected tampering while tolerating a merely absent skill.
.detail is human-readable and safe to log. It does not contain skill content or a filesystem path. There is no batch form of get_skill_result/getSkillResult. Call it per key where the outcome matters.
Security guidance
This section covers how to detect tampering, isolate privileges between the reconcile process and the agent, and account for platform and beta-specific limitations when you materialize skills onto disk.
Integrity verification and the audit log line
Every withheld skill emits one structured ERROR log record intended for security information and event management (SIEM) ingestion, regardless of your telemetry configuration.
In Python, it is on the logger launchdarkly_ai_server.skills_core.
In Node.js, it is one console.error line prefixed [LaunchDarkly].
The event name ld.skills.integrity_failure is a stability commitment. It and its field names will not change outside a major release, which means you can alert on it directly.
Each record’s reason_code field holds one of the following values:
Here’s an example of what the log record looks like:
The hash_mismatch, over_size_cap, and not_utf8 codes are the shapes of active tampering and warrant a page. The other reason codes indicate a malformed, truncated, or misbehaving payload. Skill content, filesystem paths, and credentials never appear in the record.
Privilege separation
Run the reconcile (write_skills/writeSkills) as a different identity than the agent. A SKILL.md is agent instructions, which means an agent that can write its own skills directory can rewrite its own instructions, and write access to the manifest is worse because it controls what the next reconcile may delete.
The SDK sets skill files and the manifest to mode 0644 and skill directories to 0755, with no execute bit, but those modes provide protection only if the two identities differ.
Write access to any directory above the managed root is write access to the root by another route, because it permits renaming the root aside and leaving a symlink in its place. Check each directory above the managed root, all the way up to /, and confirm the agent’s identity can write to none of them.
Platform guarantee differences
Python on POSIX and Node.js on Linux close the swap window. The managed root is opened once per reconcile and held for the duration of the call. This means a directory swapped for a symlink after its checks cannot redirect a write or a delete. Python on Windows and Node.js on macOS and Windows fall back to a per-component check immediately before each filesystem operation. This is a narrow check-then-use race, and an attacker with write permission on the managed root or its ancestors can win it. Windows is not a tested platform for materialization in this release.
Beta caveats
Some limitations for the beta version include:
- No payload signing. Delivery is TLS-only. The content hash establishes self-consistency, not origin authenticity.
- No LaunchDarkly telemetry is emitted for skills. Customer-visible detection is the log record described above.
- Do not use view-scoped SDK keys for skills. Use an environment-level SDK key.
Troubleshooting
This table includes possible errors and their solutions: