profile detection
Single source of truth for computing the set of profiles active in the current context. Consumed by six skills: `review-code`, `review-spec`, `design`, `implement`, `test`, and `document`.
Overview
Profile detection procedure
Single source of truth for computing the set of profiles active in the current context.
Consumed by six skills: review-code, review-spec, design, implement, test, and document.
Every profile under klaude-plugin/profiles/<name>/ declares its own trigger rule in DETECTION.md using the mandatory three-section schema (## Path signals, ## Filename signals, ## Content signals).
The shared procedure below applies the same algorithm against every profile's declared values.
Inputs per consuming skill
Not every consumer has a diff available. Use the input listed for your skill:
review-code— git diff (staged, or an explicit commit range). Scope is the set of files the diff touches.review-spec— git diff when invoked standalone; the feature directory's full file list when invoked byimplement(spec review runs over the whole feature, not just the current task's diff).test— git diff mid-feature, OR the feature directory's file list post-implementation.implement— the current sub-task's target file list, augmented by the diff accumulated so far in the feature.design— no file list available (implementation does not yet exist). Detection uses a user-declared or keyword-inferred signal instead; see Thedesigninteraction pattern below.document— feature directory's current file list; diff optional.
The design interaction pattern
The design phase runs before any code exists, so file-based detection is impossible. Detection uses idea-prose keyword matching against tokens declared in each profile's DETECTION.md.
Algorithm:
- Collect tokens. Iterate §Known profiles. For each
<name>,Read<plugin_root>/profiles/<name>/DETECTION.md. If the file has no## Design signalssection, skip — that profile does not participate in design-phase detection. Otherwise, parsedisplay_nameandtokensfrom the section. - Build union. Collect all declared tokens into a single set, each tagged by its source profile name and
display_name. - Match. Check the idea prose against the union. Matching is case-insensitive, whole-word (so
podin "podcast" does not fire). - Confirm. On match, surface a confirmation prompt per matched profile: "This appears to be a {display_name} feature. Activate the {profile_name} profile?" — let the user confirm yes/no. When multiple profiles match, confirm each independently.
- Fallback. If no token matches but the idea is ambiguous — names infrastructure, deployment, runtime, or platform concerns without naming a specific technology (e.g., "add a caching layer for the service", "build a CI pipeline", "deploy to production"); or includes overloaded tokens that collide across domains — build the fallback prompt dynamically from all profiles that declare
## Design signals: "Does this feature involve {display_name_1, display_name_2, ...}? If yes, which?"
Confirmation is required — the design skill never auto-activates a profile silently. The narrow per-profile token sets avoid noisy false positives from tokens that overload across domains.
Once activated, subsequent design-phase steps treat the profile as active in the same record shape produced by file-based detection (see §Output shape).
Known profiles
This is the authoritative enumeration of profile <name>s — do NOT try discover profiles via any other means.
An explicit list is boring, deterministic, and unambiguous; runtime filesystem enumeration against the plugin tree has proven unreliable.
gopythonjavajs_tskotlink8sk8s-operator
Algorithm
Throughout this procedure, <plugin_root> denotes the absolute plugin-root path the agent already knows from the SKILL.md that invoked the procedure. Use that resolved absolute path wherever <plugin_root> appears.
-
Iterate profiles. For each §Known profiles
<name>:- Use the
Readtool on<plugin_root>/profiles/<name>/DETECTION.md. - If
Readfails with ENOENT (profile name in list but directory missing — a stale list entry), skip silently and move on. - If
Readsucceeds, parse the declared## Path signals,## Filename signals, and## Content signalssections.
- Use the
-
Evaluate in cost order. For each input file, check signals in this order: path → filename → content. Cheapest first.
-
Apply the authority rule. A file activates the profile only if a filename signal OR content signal matches. A path-only match does NOT activate. Paths are a pre-filter that promotes files to "candidates"; authoritative activation requires filename or content confirmation. A file that matches NO path signal is still evaluated against filename and content signals — path pre-filtering is a cost hint, not a gate. (Otherwise a
Chart.yamlat a non-standard path would be missed.) -
Bound content inspection. Read at most ~16 KB per file when evaluating content signals. Multi-document YAML is inspected per
----separated block — a file may have five blocks, and only the third need match for the file to activate the profile. -
Collect records. Accumulate one record per matched profile with the triggering files and the signal descriptions that fired.
Tool choice
- Single file at
<plugin_root>/…→Read. This is what the algorithm uses. - Enumeration across profiles → iterate the §Known profiles list,
Readeach. NeverGlob(cwd-scoped, misses outside-cwd paths).
Two dimensions: cost vs authority
Signals live on two axes that point in different directions. Keep them separate in your mental model:
- Evaluation cost (cheapest first): path < filename < content. Path globs touch only the path string; filename matches are exact string compares; content inspection opens the file.
- Authority (most authoritative first): filename ≈ content > path. A filename or content match activates the profile; a path-only match does not. Filename and content are equally authoritative, but filename resolves first at runtime — a filename match short-circuits content inspection for that file.
Evaluating cheapest-first optimizes work. Applying authority correctly prevents false positives from incidental path matches — a stray manifests/ directory in a Go project does not make the project Kubernetes.
Plugin-root resolution failure
If every Read attempt in Algorithm step 1 fails — i.e., <plugin_root> could not be resolved from SKILL.md context, or the paths do not exist — the procedure cannot continue.
On that failure:
- Emit an actionable error pointing to
CLAUDE.md§Profile Conventions. - Return an empty result set so the calling skill falls back to generic guidance rather than panicking.
- Do not retry; do not silently guess a path.
Consumers inherit this check by invoking the shared procedure — no skill re-implements it.
Output shape
A list of records, one per matched profile:
[
{
profile: "<name>", // directory name under profiles/
triggered_by: [
"filename: Chart.yaml", // signal type + matched value
"content: apiVersion+kind in block 2"
],
files: [
"path/to/file1.yaml",
"path/to/file2.yaml"
]
},
...
]
Field semantics:
profile— the directory name underprofiles/(e.g.,go,python,k8s). Used downstream to resolveprofiles/<profile>/<phase>/index.md, where<phase>is the profile phase subdirectory named identically to the calling skill:review-code/,review-spec/,design/,implement/,test/, ordocument/.triggered_by— which signal type fired and the specific value that matched. For debugging and for explaining detection to the user; never used as the key for profile lookup.files— the subset of input files that activated this profile. Skills use this to scope behavior (e.g.,helm lintruns only on files triggered under Helm filename signals, not on every YAML in the diff).
When no profile matches, return the empty list []. The caller falls back to generic guidance, identical to today's "no language detected" path.