Skip to content

8 min read

Nothing declares a dependency on a skill that simply fires

On this page

The obvious first move, once Marta accepted that the narrowing needed announcing, was to find out who would be affected. She searched the monorepo for svc-scaffold and got eleven matches: seven in her own repository, three in team READMEs that mentioned it as a recommended tool, and one in a stale onboarding doc. She searched for the names of the templates it emits and got the generated files themselves, which tells you the skill ran at some point in the past and nothing about whether anyone relies on it now. She asked in #eng-tooling whether anyone used the skill for modifying existing services, and got two thumbs-up reactions and no replies.

None of that is a dependency graph, and no amount of searching would have produced one, because the relationship it would describe was never written down anywhere. This is the specific way a shared skill differs from a shared library, and it is worth being precise about the mechanism rather than treating it as a general fuzziness.

There is no call site to find

When a service depends on httpx, someone typed import httpx into a file that is in version control. That line is a declaration: it is machine-readable, it is attributable to a repository and therefore to a team, and it exists whether or not the code has run recently. Tooling can walk it. A maintainer planning a breaking change can produce a list of dependent repositories in a minute, and the list is complete in a way that requires no judgement.

A skill has no equivalent line. The consumer wrote a sentence — "add a /refunds endpoint to billing-api" — into a session, and something in the harness — the program wrapped around the model that assembles its context, chooses which skills to load into it and executes its tool calls — matched that sentence against a set of descriptions and decided this skill applied. The decision is made at runtime, from natural language, per turn. The nearest thing to a call site is the user's phrasing, and it lives in a transcript in someone else's workspace rather than in a repository you can clone. The relationship is real, and it has the same operational weight as an import, with none of the same evidence attached.

The shared MCP server from the previous chapter had it better, and the difference is instructive. The registry team could count reads of the owner field because every one of those reads was an inbound request to a process they operate, carrying a client identity. Observability follows from being on the receiving end of a call. A skill is not called; it is loaded into a context by the consumer's own harness, so the only place the event exists is the consumer's machine.

Flowchart: User's request → Agent harness; Agent harness → Skill loaded (selects skill from description); Agent harness → Shared MCP server (tool call); Skill loaded → nothing; Shared MCP server → Server access log client id, tool, args.

The asymmetry in that picture is the whole problem. The right-hand path produces a record on infrastructure the maintainer runs. The left-hand path produces a record nowhere, unless somebody deliberately puts one there.

Manufacturing the record

Activation telemetry is that deliberate step: an event, written by the consumer's harness at the moment a skill is loaded, and shipped somewhere the maintainer can query. Most agent harnesses expose a hook for this — a command the harness runs by itself at a named moment in a session, configured rather than coded. In Claude Code the shared configuration can register a hook that runs when a skill activates, and the hook does nothing more interesting than append a line to a file and let a collector ship it.

json
{
  "hooks": {
    "SkillActivated": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "log-skill-activation" }
        ]
      }
    ]
  }
}
python
#!/usr/bin/env python3
"""log-skill-activation: append one JSONL record per skill activation."""
import json, os, sys, time, hashlib

event = json.load(sys.stdin)          # harness supplies the activation payload
request = event.get("user_request", "")

record = {
    "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    "skill": event["skill_name"],
    "version": event.get("skill_version", "unknown"),
    "team": os.environ.get("TEAM_SLUG", "unset"),
    "repo": os.environ.get("REPO_NAME", "unset"),
    # a stable id for de-duplication, not the text itself
    "request_id": hashlib.sha256(request.encode()).hexdigest()[:12],
    "intent": classify(request),      # small local classifier, see below
    "request_len": len(request),
}

with open(os.path.expanduser("~/.agent-telemetry/activations.jsonl"), "a") as f:
    f.write(json.dumps(record) + "\n")

The field that does the work is intent, and the reason it is a classification rather than the raw request is that shipping colleagues' prompts to a central store is a data-handling decision with consequences well beyond this problem. A dozen buckets chosen by the skill's maintainer — create_service, modify_existing, add_endpoint, refactor, unclear — are enough to answer the question at hand and are defensible to a privacy review in a way that a transcript archive is not. Marta's classifier was forty lines of keyword rules with an unclear bucket that caught 18% of requests, which was good enough; a mis-bucketed 18% would have had to be very unevenly distributed to change any of the conclusions she drew.

Thirty days of that produced 2,318 activations of svc-scaffold across four teams. Split by the boundary she was about to draw, the answer was not the one the three bad pull requests had suggested:

TeamActivationsWould still fire after narrowingWould stop firingShare dropped
platform604561437%
payments81219062277%
ledger4974029519%
growth405388174%
total2,3181,54177734%

Three of the four teams would barely have noticed. The payments team was using the skill for something other than what it said on the label, three-quarters of the time, and they were the team whose readiness checks broke. That concentration is typical: dependence on an under-specified behaviour tends to pool in one or two consumers who have quietly built a workflow around it, rather than spreading evenly, which means an aggregate number like "34% of activations" understates the impact on the team that matters and overstates it for everyone else. The per-team split is the number worth having.

Where the harness offers no activation hook at all, a weaker version of the same record can be stamped into the output. A skill whose templates emit a header comment naming the skill and its version — # generated by svc-scaffold 0.9.2 — leaves a trace in the consumer's repository that a search across the organisation can find, which converts an unanswerable question into a slow but answerable one. It undercounts badly, since it only sees uses that produced a committed file and misses every case where the skill supplied guidance a human then typed by hand, and it is biased toward exactly the heavy code-generating uses that are easiest to notice anyway. It is still better than eleven grep hits in your own repository, and it costs one line in a template.

Asking people who cannot see their own dependency

Telemetry answers this cleanly and it usually does not exist yet, because nobody instruments a skill until the first time they need this. The fallback is asking the consuming teams, and the way the question is put determines whether the answer is worth anything.

Marta's first attempt was the natural one:

text
Heads-up: I'm narrowing svc-scaffold's description so it only applies to
new service creation, not modifications to existing services. It was
over-applying on edits. Shipping next week — shout if this affects you.

Five teams read that. Nobody shouted. The message is accurate and it asks the reader to perform a translation they are not equipped to perform: to recall which of their requests, over months, were phrased in a way that a description-matching step would have classified as "modification", and to predict which of those would fall on the far side of a boundary they cannot see. A consumer of a skill has no more visibility into activation than the maintainer does, and less reason to have thought about it. Abstract statements of a dependency are unrecognisable to the people who hold it.

The second attempt carried examples instead:

text
svc-scaffold will stop firing on requests like these:

  - "add a /refunds endpoint to billing-api"
  - "add a new consumer to the events service"
  - "wire up the readiness probe on ledger-api"
  - "extend checkout-api with the new webhook"
  - "add health endpoints to the reporting service"

It will keep firing on "create a new service ...", "scaffold a ... service",
"stand up a new Python service".

If your team asks for anything in the first list, reply here and tell me
what you get out of it today.

Three of the five teams replied within a day, and the payments team's reply named the thing nobody had written down: they used it for endpoint work specifically to get the readiness handler right, and they deleted the rest of what it generated. Recognition is a much easier cognitive task than recall, and the examples convert the question from recall into recognition. The eight phrasings cost twenty minutes to assemble from the sampled activations, and where there is no telemetry they can be reconstructed from the skill's own eval cases and from a handful of pull requests it plainly touched.

Both channels have a limit that should be stated rather than glossed. Telemetry is retrospective and frequency-weighted: a behaviour used once a quarter by the team that runs the year-end close is invisible in thirty days of data, and it can still be the most expensive thing to break. Asking is subject to the same bias plus availability — people report what they did this week. The practical consequence is that neither the query nor the thread proves absence of dependence. What they produce is a list of known consumers, and the correct posture toward the unknown remainder is the additive, dated migration path from the previous chapter rather than confidence that nobody is left.

There is also a cost to putting this record in place, and it is not the code. Activation telemetry means a hook running on every engineer's machine writing a line about what they asked for, and that requires a conversation about what is recorded, who can query it, and how long it is kept, before it requires a collector. A store that keeps intent buckets and team slugs for 90 days is straightforward to justify. One that keeps request text indefinitely will be discovered by someone in a security review, and its removal will take the useful part with it.

What the record is for after the announcement

The list of behaviours consumers actually rely on is the same list that ought to be under test. Marta's eval cases had been written from what she remembered building: service creation for four language stacks, three tiers, a couple of edge cases around naming. Nothing in that suite exercised endpoint addition on an existing service, because she had never regarded it as a supported use, which is exactly why it could degrade for eleven days without anything going red. The 777 activations that were about to stop firing describe a set of behaviours that were, in practice, part of the contract, and the ones being kept describe the contract going forward.

That reframes what a test on a shared component is for. It is not a check that the maintainer's intended behaviour still works, which is the version 12.6 covered for a skill you own alone. On something four teams load, the suite has to cover the behaviours consumers depend on, discovered rather than remembered — and it has to be attached to a released artefact, because a test that catches a regression in a repository nobody can name a version of still leaves the affected team with a calendar and a guess.

Distribution and reuse — read the current version at https://daviddominguez.dev/learn/en/distribution-and-reuse/nothing-declares-a-dependency-on-a-skill-that-simply-fires

Back to top