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
bytes in Python and Uint8Array in 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’s name and description come 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:
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.
| Pattern | Use cases | SDK outputs | API |
|---|
| Filesystem agents | Claude Agent SDK, Codex, or anything that discovers a skills/ directory | Skill files written to disk before the agent starts, kept current with LaunchDarkly | write_skills/writeSkills, optionally watch_skills/watchSkills |
| Dynamic loaders | Custom harnesses, LangChain-style skill-loading tools | Verified skill bytes on demand at runtime | get_skill/getSkill and related functions |
| Provider-uploaded references | Anthropic’s container.skills, OpenAI skill references | The list of pinned skill references on the resolved variation, to pass through in the model call. No content is fetched. | skill_refs/skillRefs only. Needs no delivery setup. |
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 configure with set_skill_store. The production store is FDv2SkillStore. Start it, wait for the first payload, then set it as the skill store: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 and "*", a revoked skill has its SKILL.md pruned from the managed root instead of at the next restart. With an explicit list, such as skill_refs(info["config"]), a skill deleted in LaunchDarkly is reported as an error action, and its SKILL.md is kept instead of pruned. Because the watcher only listens to the skill store and not to flag changes, it does not detect a skill removed from a variation.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 configure with setSkillStore. The production store is FDv2SkillStore. Start it, wait for the first payload, then set it as the skill store: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 and '*', a revoked skill’s SKILL.md is pruned from the managed root within seconds instead of at the next restart. With an explicit list, such as skillRefs(info.config), a skill deleted in LaunchDarkly is reported as an error action, and its SKILL.md is kept instead of pruned. Because the watcher listens only to the skill store and not to flag changes, it does not detect a skill removed from a variation.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 | Node.js | Description | Default value |
|---|
mode | mode | ”stream” or “poll”. | “stream” |
base_uri | baseUri | Must be https://. Where poll requests are sent. Override for private instances or proxies that serve the FDv2 endpoints. Passed on its own, it applies to both polling and streaming, which is what a single-host relay or private instance needs. | https://sdk.launchdarkly.com |
stream_uri | streamUri | Must be https://. Where streaming requests are sent. Polling and streaming are separate hosts at LaunchDarkly, which means the defaults are a pair. Set this as well as base_uri only when the two differ. | https://stream.launchdarkly.com |
poll_interval (seconds) | pollIntervalSeconds | Poll mode only. | 30 seconds |
initial_backoff, max_backoff (seconds) | initialBackoffSeconds, maxBackoffSeconds | Reconnect backoff. max_backoff caps every delay between retries, including one the server asks for with Retry-After. Recoverable failures are retried for as long as the store runs. Only a fatal error, such as an unauthorized key, stops delivery, and failed reports it. | 1s / 30s |
read_timeout (seconds) | readTimeoutSeconds | The single network timeout in both SDKs. In poll mode, it bounds the whole request. In stream mode, it bounds the wait for the next bytes, which means a stream that goes quiet past it reconnects rather than hanging. There is no separate connect timeout. | 10s in poll mode, 300s in stream mode |
The watcher debounce uses different time unitsStore options, wait_for_skills/waitForSkills, and the timeout option on write_skills/writeSkills are in seconds in both languages. The watch_skills/watchSkills debounce is the exception. debounce takes 0.5 seconds in Python, and debounceMs takes 500 milliseconds in Node.js, in the same options object as timeout.
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/writeSkills records what it owns in a manifest at <root>/.launchdarkly-skills.json and only ever overwrites or deletes paths that the manifest lists. If you place a file yourself at a managed path, it’s reported as an error and 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
error action.
- A requested skill that was never delivered is an error. In that case,
report.ok is false. 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:
| Option | Default value | Description |
|---|
prune | True | Remove previously managed skills that are no longer in the requested set. This is also how revocation reaches disk. |
timeout | 10 seconds, both languages | Total budget for the call, including retrieval. |
on_unavailable/onUnavailable | ”keep" | "keep” leaves last-known-good managed files in place and reports the failure. “raise” raises or throws instead, for callers that refuse to start on stale content. |
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.
| Reason | Meaning |
|---|
ok | A verified skill was returned. |
absent | The store holds nothing under that key. It is either not configured, not yet delivered, or revoked. A pinned version the store does not yet hold also lands here, which is the ordinary rollout-skew case. |
wrong_version | A version was pinned and the store answered with a different one, which means the answer was withheld. This means a store is misbehaving, usually a custom store adapter, rather than a version not having arrived yet. |
integrity_failure | Content was delivered and failed verification, which means it was withheld. Treat this one as a hard stop rather than continuing in a degraded state. |
store_unavailable | The store could not answer at all due to an outage. |
.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].
While agent skills is experimental, the event name ld.skills.integrity_failure, its fields, and its reason_code values may change in a minor release. Any such change gets a changelog entry under Experimental. If you alert on this record, review the changelog before you upgrade.
Each record’s reason_code field holds one of the following values:
| reason_code | Meaning |
|---|
hash_mismatch | The downloaded content’s SHA-256 hash didn’t match the hash LaunchDarkly delivered. |
invalid_key | A key in the delivered response fails format validation. It is limited to 256 characters. The key can contain lowercase letters, numerals, or hyphens. |
invalid_version | The version number associated with a skill was a non-integer, pointing to a problem with the skill provider. |
key_mismatch | The store returned a payload whose embedded key doesn’t match the key that was requested. |
missing_content | The store entry has no content. |
missing_content_hash | The store entry has content but no hash to verify it against. |
not_an_object | The raw store entry is not shaped as a valid object. |
not_utf8 | The content could not be encoded as UTF-8. |
over_size_cap | The content exceeds the SDK’s size limit and was rejected. |
version_mismatch | The store returned a version other than the one pinned by the reference. |
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.
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:
| Symptom | Likely cause | Resolution |
|---|
| Store reports a fatal error naming HTTP 403 | FDv2 delivery is not enabled for your account. | Contact LaunchDarkly support or your account team to enable FDv2. |
| Store reports a fatal error naming HTTP 422 | The SDK key is view-scoped. | View-scoped SDK keys don’t work with skills. Use an environment-level SDK key. |
| HTTP 401 at start | Wrong credential. | Use the environment’s server-side SDK key. |
| Constructor raises or throws about a mobile key or client-side ID | Non-server-side credential. | Skills are server-side only. |
| HTTP 404, reported as fatal | Wrong base_uri/stream_uri, or the instance does not serve the FDv2 endpoints. | Check both URIs. Relay Proxy does not serve these endpoints. |
| A fatal error naming a 3xx redirect | A proxy or private instance is redirecting the FDv2 endpoint. | Redirects are never followed, which means your SDK key is never forwarded to another host. Check the URI and any proxy in between. |
| Constructor rejects the URI | A plain http:// base_uri or stream_uri. | Both must be https://. Loopback hosts are the only exception, for local test doubles. |
get_skill/getSkill immediately after start() returns nothing | The first payload has not arrived. | Call wait_for_skills/waitForSkills before the first retrieval, and check its return value. |
report.ok is false and the error says the retrieval was unavailable, with nothing written | The reconcile ran before delivery answered. | This prevents an empty store from reading as “everything was revoked.” Gate the reconcile on wait_for_skills/waitForSkills. |
A ValueError or thrown error from write_skills/writeSkills naming the root | The root’s parent is missing, the root is a file, or the root is a symlink. | Create the parent directory, and point at a real directory. |
A skill is reported as error with “not managed” wording and left untouched | A file you placed at a managed path collides with a skill, and its bytes differ from the delivered content. | Move or remove your file. The SDK never overwrites unmanaged files. A file that already matches the delivered bytes exactly is adopted instead and reported skipped_current. |
| A revoked skill is still on disk | A one-shot write_skills/writeSkills call with no watcher, poll mode, or an explicit skill list. | Use watch_skills/watchSkills with ”*” and streaming for seconds-latency pruning. With an explicit list, re-run write_skills/writeSkills with a fresh list to remove a skill unpinned from the variation. A skill deleted in LaunchDarkly that the variation still references stays on the list, is reported as an error action, and is kept. Use ”*” when revocation must reach the disk. Otherwise, pruning happens at the next reconcile. |
start() raises on a store that was working earlier | The store was closed. close() is final. | Construct a new store. A closed one still answers from cached content but cannot resume delivery. |