Best practice for Git-managed Deep Agents skills and thread versioning

Hi everyone,

We use deepagents==0.6.8 on LangSmith Deployment and are looking for the recommended architecture for skills.

Our skills are immutable at runtime. Git is the source of truth: changes are reviewed through GitHub and validated by CI. Normal users and the agent must
never modify them.

We considered three options:

  1. FilesystemBackend
    The simplest solution would be to expose the packaged agent/skills/ directory as a read-only route in a CompositeBackend. However, the documentation
    discourages FilesystemBackend for web servers and APIs. Does this warning still apply when the backend is restricted to a non-secret, read-only skills
    directory with virtual_mode=True?

  2. ContextHubBackend
    Context Hub provides native versioning, but Git would no longer be the only editable source. We would still need Git-to-Context-Hub synchronization,
    runtime LangSmith credentials, and potentially an agent repo linking several skill repos. This seems unnecessary when the skills belong only to our
    application.

  3. StoreBackend
    This is our current choice. Before local startup or Cloud deployment, we seed the Git skills into the Store and deny agent writes under /skills/**. It
    works, but requires custom synchronization, versioned namespaces, cleanup, deployment credentials, and rolling-deployment coordination.

Is StoreBackend the recommended pattern for immutable, Git-managed skills in a custom LangSmith Deployment, or is there a simpler supported mechanism?

We also need guidance on versioning existing threads.

SkillsMiddleware stores skills_metadata in checkpointed state and loads it only once per session, while the full SKILL.md is read lazily.

If Thread A starts with skills v1 and we deploy v2, should:

  • Thread A remain pinned to v1 while new threads use v2?
  • Or should Thread A automatically use v2 on its next invocation?

Is there a supported mechanism to pin or refresh skill versions without mixing old metadata with new skill content?

Our goal is a simple, deterministic setup where Git remains authoritative.

hi @beubeu13220

this is a great question!
I’ve been having a similar question lately. Let me gather my thoughts and I’ll get back to you shortly.

So what I think now.

It feels like StoreBackend is the right answer.
Your current setup (CompositeBackend routing /skills/ to StoreBackend, deny-write FilesystemPermission) is show almost verbatim in the official skills docs as the pattern for shared, agent-immutable skills.
I think there is no simpler supported mechanism.

FilesystemBackend is technically defensible in your scoped case (virtual_mode=true blocks path escapes, no secrets in root_dir, ephemeral container), but the production docs flatly say “Don’t use them in deployed agents,” there’s no backend-level read-only mode (deny rules are tool-level only), and it can’t pin old threads to old skill versions since redeploys replace the files for everyone.

ContextHubBackend - I agree it’s unnecessary for app-owned skills, and found an extra source-level caveat: it caches the entire hub repo in-process and never invalidates on success, so a long-lived deployment would serve stale content anyway. Its native versioning doesn’t wire into per-thread pinning either.

Thread versioning - in skills.py file I see that skills_metadata is checkpointed per thread and never reloaded, while SKILL.md bodies are read lazily at the path recorded in that frozen metadata. I think there may be two workarounds:

  • pin: version-in-path - seed /skills/<git-sha>/…, pass skills=[f"/skills/{VERSION}/"], seed v2 before rolling the revision. Old threads stay fully v1 (your recorded paths still resolve), new threads get v2, and the rolling-deployment race disappears.

  • refresh: a tiny SkillsMiddleware subclass that keys the cache on a skills_version stamp instead of mere key presence, or per-thread client.threads.update_state().

So I would say that the best solution for your case for now is:

stay with what he already has (StoreBackend) and move the version into the path

  agent = create_deep_agent(
      backend=CompositeBackend(
          default=StateBackend(),
          routes={"/skills/": StoreBackend(namespace=lambda rt: ("skills",))},
      ),
      skills=[f"/skills/{SKILLS_VERSION}/"],   # SKILLS_VERSION = SHA/tag
      permissions=[FilesystemPermission(operations=["write"], paths=["/skills/**"], mode="deny")],
  )

Pipeline:

  • Merge to main - CI validates the skills → seeds them into the Store under /skills//… - never overwriting existing prefixes.
  • Only then rolls out the deployment revision that carries that SHA in an env var.
  • A separate job cleans up old version prefixes once the thread retention window has passed

The costs that remain - and are irreducible: the seeding job (with credentials only in CI, not at runtime), garbage collection of old versions, and accepting that old threads will not receive skill fixes (if you ever want latest-wins for content, add that SkillsMiddleware subclass with a version stamp - but by default, pinning serves your stated “deterministic” goal better imo).

This is my conclusion for now. And I’m open to further input and changes to my perspective

Thanks, this is very helpful. Given the tools currently available, I agree that your StoreBackend approach with versioned paths seems to be the most appropriate.

I still have two concerns:

  • How can we safely prune an old skills SHA? Thread TTL is inactivity-based: using an old thread again extends its expiration. We therefore cannot simply delete a version after deployment date + thread TTL. Is there a native way to know whether a SHA is still referenced by resumable threads?

  • Is per-thread pinning worth this complexity? Codex and Claude Code generally use the current skills rather than retaining historical versions per conversation. A latest-wins policy would avoid maintaining multiple versions, reference tracking and garbage collection while applying fixes immediately.

I’m surprised that immutable Git-managed skills require this much custom lifecycle management in LangSmith. Are we missing a simpler intended workflow, or are Store seeding, versioning and cleanup currently the expected production pattern?