Describe the bug
After a fresh install of microsoft/azure-skills into a project with a .claude/ marker dir, .claude/settings.json contains four distinct problems:
- Cursor's
${CURSOR_PLUGIN_ROOT} variable inside Claude Code's config
- Bare
${PLUGIN_ROOT} (no tool prefix) that Claude won't expand
- Cursor's camelCase
postToolUse event key emitted alongside Claude's PascalCase PostToolUse
- Every hook entry written twice
Net effect: of the four PostToolUse entries written, only one is well-formed and will execute in Claude Code; the other three are dead config — three different failure modes — and the entire postToolUse (camelCase) block is unreachable in Claude.
To Reproduce
- Create empty project:
mkdir -p ~/temp/apm-repro && cd ~/temp/apm-repro
- Initialize APM:
apm init
- Add marketplace:
apm marketplace add github/awesome-copilot
- Create Claude marker dir:
mkdir .claude
- Install:
apm install microsoft/azure-skills/.github/plugins/azure-skills
- Inspect:
cat .claude/settings.json
Expected behavior
.claude/settings.json should contain only Claude-formatted hooks under the PascalCase PostToolUse key.
- All paths should use
${CLAUDE_PLUGIN_ROOT} (which Claude expands) or fully-resolved literal paths — never ${CURSOR_PLUGIN_ROOT} or bare ${PLUGIN_ROOT}.
- Each unique hook should appear exactly once.
- Cursor-formatted hooks belong in
.cursor/hooks.json, not in Claude's settings.
Environment
- OS: macOS 26.4.1 (Apple Silicon)
- Python Version: 3.14.0
- APM Version: 0.9.4 (latest, released 2026-04-27 ~6 hours before this report)
- VSCode Version: N/A (CLI behavior)
Logs
Full .claude/settings.json after the repro above (verbatim, 48 lines):
{
"hooks": {
"PostToolUse": [
{
"type": "command",
"bash": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh",
"powershell": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.ps1",
"_apm_source": "azure-skills"
},
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/azure-skills/hooks/scripts/track-telemetry.sh"
}
],
"_apm_source": "azure-skills"
},
{
"type": "command",
"bash": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh",
"powershell": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.ps1",
"_apm_source": "azure-skills"
},
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/azure-skills/hooks/scripts/track-telemetry.sh"
}
],
"_apm_source": "azure-skills"
}
],
"postToolUse": [
{
"type": "command",
"command": "bash ${CURSOR_PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh",
"_apm_source": "azure-skills"
},
{
"type": "command",
"command": "bash ${CURSOR_PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh",
"_apm_source": "azure-skills"
}
]
}
}
apm.lock.yaml deployed_files excerpt (showing duplicates from a single install):
deployed_files:
- .github/hooks/azure-skills-copilot-hooks.json
- .github/hooks/azure-skills-copilot-hooks.json
- .github/hooks/azure-skills-cursor-hooks.json
- .github/hooks/azure-skills-cursor-hooks.json
- .github/hooks/azure-skills-hooks.json
- .github/hooks/azure-skills-hooks.json
- .github/hooks/scripts/azure-skills/hooks/scripts/track-telemetry.sh
- .github/hooks/scripts/azure-skills/hooks/scripts/track-telemetry.sh
Additional context
Plugin and marketplace
- Plugin:
microsoft/azure-skills/.github/plugins/azure-skills @ e98cf5c5ee6a34ff5085a0a74693e3f224b55017
- Marketplace:
github/awesome-copilot
Findings broken down
1. Cursor's variable leaked into Claude's config (settings.json lines 38, 43)
"command": "bash ${CURSOR_PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh"
Claude Code does not expand ${CURSOR_PLUGIN_ROOT}. These hook commands would fail at execution time — though they're moot because they live under the postToolUse (camelCase) key that Claude never reads (see finding 3).
2. Bare ${PLUGIN_ROOT} and unrecognized keys (settings.json lines 6, 7, 21, 22)
"bash": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh",
"powershell": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.ps1",
Three distinct variable conventions appear in the same file (${PLUGIN_ROOT}, ${CURSOR_PLUGIN_ROOT}, and the literal .claude/hooks/... paths on lines 14, 29). Per src/apm_cli/integration/hook_integrator.py (lines 40, 267, 298), the canonical source convention APM expects is ${CLAUDE_PLUGIN_ROOT} — none of the three variants in the output match.
Additionally, bash / powershell keys at the same level as type: command don't conform to Claude Code's hook schema. This looks like a Cursor cross-platform-shell convention emitted into Claude's file.
3. Both PostToolUse and postToolUse keys present
Claude uses PascalCase event names (PostToolUse). Cursor uses camelCase (postToolUse). Both keys appear in .claude/settings.json. Claude only reads the PascalCase one, so the camelCase block is dead — and it's where the ${CURSOR_PLUGIN_ROOT} leak lives. The Cursor-formatted block should never have been written into .claude/settings.json at all.
4. Every hook entry written twice
After de-duplication, there are only two unique entries in PostToolUse and one unique entry in postToolUse, each appearing twice. The lockfile (logs section above) shows the same pattern in deployed_files. This appears distinct from the duplicate-hook bug fixed in #708 — that one was about re-running apm install appending entries. These duplicates appear after a single install, suggesting a different code path is iterating over hook sources twice (possibly: once in an "all tools" pass and once in a Claude-specific pass).
Hypothesis
src/apm_cli/integration/hook_integrator.py knows about ${CLAUDE_PLUGIN_ROOT} as the canonical source variable (line 40 comment: "resolved relative to package root, rewritten for target"). The bug appears to be in the emit path for the Claude target:
- Claude target receives entries from multiple source-format passes (Cursor format + Claude format + raw Copilot format) without filtering to Claude's schema.
- The "rewrite for target" step doesn't appear to run on at least some of those entries —
${CURSOR_PLUGIN_ROOT} and ${PLUGIN_ROOT} should both have been normalized before emission.
I haven't traced the exact code path; the symptoms are visible in the output without that.
Related
Why it matters
microsoft/azure-skills is a Microsoft-published plugin in the awesome-copilot marketplace, so this affects every Claude Code user installing Azure agent skills via APM. The visible failure mode is "the hook silently doesn't run" — Claude reads the only well-formed entry, ignores the malformed duplicates, and never touches the camelCase block. End users may not realize their telemetry hooks aren't firing.
Describe the bug
After a fresh install of
microsoft/azure-skillsinto a project with a.claude/marker dir,.claude/settings.jsoncontains four distinct problems:${CURSOR_PLUGIN_ROOT}variable inside Claude Code's config${PLUGIN_ROOT}(no tool prefix) that Claude won't expandpostToolUseevent key emitted alongside Claude's PascalCasePostToolUseNet effect: of the four
PostToolUseentries written, only one is well-formed and will execute in Claude Code; the other three are dead config — three different failure modes — and the entirepostToolUse(camelCase) block is unreachable in Claude.To Reproduce
mkdir -p ~/temp/apm-repro && cd ~/temp/apm-reproapm initapm marketplace add github/awesome-copilotmkdir .claudeapm install microsoft/azure-skills/.github/plugins/azure-skillscat .claude/settings.jsonExpected behavior
.claude/settings.jsonshould contain only Claude-formatted hooks under the PascalCasePostToolUsekey.${CLAUDE_PLUGIN_ROOT}(which Claude expands) or fully-resolved literal paths — never${CURSOR_PLUGIN_ROOT}or bare${PLUGIN_ROOT}..cursor/hooks.json, not in Claude's settings.Environment
Logs
Full
.claude/settings.jsonafter the repro above (verbatim, 48 lines):{ "hooks": { "PostToolUse": [ { "type": "command", "bash": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh", "powershell": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.ps1", "_apm_source": "azure-skills" }, { "hooks": [ { "type": "command", "command": "bash .claude/hooks/azure-skills/hooks/scripts/track-telemetry.sh" } ], "_apm_source": "azure-skills" }, { "type": "command", "bash": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh", "powershell": "${PLUGIN_ROOT}/hooks/scripts/track-telemetry.ps1", "_apm_source": "azure-skills" }, { "hooks": [ { "type": "command", "command": "bash .claude/hooks/azure-skills/hooks/scripts/track-telemetry.sh" } ], "_apm_source": "azure-skills" } ], "postToolUse": [ { "type": "command", "command": "bash ${CURSOR_PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh", "_apm_source": "azure-skills" }, { "type": "command", "command": "bash ${CURSOR_PLUGIN_ROOT}/hooks/scripts/track-telemetry.sh", "_apm_source": "azure-skills" } ] } }apm.lock.yamldeployed_filesexcerpt (showing duplicates from a single install):Additional context
Plugin and marketplace
microsoft/azure-skills/.github/plugins/azure-skills@e98cf5c5ee6a34ff5085a0a74693e3f224b55017github/awesome-copilotFindings broken down
1. Cursor's variable leaked into Claude's config (settings.json lines 38, 43)
Claude Code does not expand
${CURSOR_PLUGIN_ROOT}. These hook commands would fail at execution time — though they're moot because they live under thepostToolUse(camelCase) key that Claude never reads (see finding 3).2. Bare
${PLUGIN_ROOT}and unrecognized keys (settings.json lines 6, 7, 21, 22)Three distinct variable conventions appear in the same file (
${PLUGIN_ROOT},${CURSOR_PLUGIN_ROOT}, and the literal.claude/hooks/...paths on lines 14, 29). Persrc/apm_cli/integration/hook_integrator.py(lines 40, 267, 298), the canonical source convention APM expects is${CLAUDE_PLUGIN_ROOT}— none of the three variants in the output match.Additionally,
bash/powershellkeys at the same level astype: commanddon't conform to Claude Code's hook schema. This looks like a Cursor cross-platform-shell convention emitted into Claude's file.3. Both
PostToolUseandpostToolUsekeys presentClaude uses PascalCase event names (
PostToolUse). Cursor uses camelCase (postToolUse). Both keys appear in.claude/settings.json. Claude only reads the PascalCase one, so the camelCase block is dead — and it's where the${CURSOR_PLUGIN_ROOT}leak lives. The Cursor-formatted block should never have been written into.claude/settings.jsonat all.4. Every hook entry written twice
After de-duplication, there are only two unique entries in
PostToolUseand one unique entry inpostToolUse, each appearing twice. The lockfile (logs section above) shows the same pattern indeployed_files. This appears distinct from the duplicate-hook bug fixed in #708 — that one was about re-runningapm installappending entries. These duplicates appear after a single install, suggesting a different code path is iterating over hook sources twice (possibly: once in an "all tools" pass and once in a Claude-specific pass).Hypothesis
src/apm_cli/integration/hook_integrator.pyknows about${CLAUDE_PLUGIN_ROOT}as the canonical source variable (line 40 comment: "resolved relative to package root, rewritten for target"). The bug appears to be in the emit path for the Claude target:${CURSOR_PLUGIN_ROOT}and${PLUGIN_ROOT}should both have been normalized before emission.I haven't traced the exact code path; the symptoms are visible in the output without that.
Related
apm installduplicates hook entries in settings.json #708 (closed 2026-04-15) — duplicates on re-install. Different mechanism but same symptom class. Worth re-checking the test coverage there to see if the fix only covered the re-install path.Why it matters
microsoft/azure-skillsis a Microsoft-published plugin in the awesome-copilot marketplace, so this affects every Claude Code user installing Azure agent skills via APM. The visible failure mode is "the hook silently doesn't run" — Claude reads the only well-formed entry, ignores the malformed duplicates, and never touches the camelCase block. End users may not realize their telemetry hooks aren't firing.