Skip to content

[BUG] .claude/settings.json emits Cursor-formatted hooks, bare ${PLUGIN_ROOT}, and duplicate entries after fresh install of microsoft/azure-skills #1007

@aaronrogers

Description

@aaronrogers

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:

  1. Cursor's ${CURSOR_PLUGIN_ROOT} variable inside Claude Code's config
  2. Bare ${PLUGIN_ROOT} (no tool prefix) that Claude won't expand
  3. Cursor's camelCase postToolUse event key emitted alongside Claude's PascalCase PostToolUse
  4. 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

  1. Create empty project: mkdir -p ~/temp/apm-repro && cd ~/temp/apm-repro
  2. Initialize APM: apm init
  3. Add marketplace: apm marketplace add github/awesome-copilot
  4. Create Claude marker dir: mkdir .claude
  5. Install: apm install microsoft/azure-skills/.github/plugins/azure-skills
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/cliCLI command surface, flags, help text (cross-cutting).area/multi-targetMulti-target deploy spec, target directory creation, agent surface routing.priority/highShips in current or next milestonestatus/acceptedDirection approved, safe to start work.status/triagedInitial agentic triage complete; pending maintainer ratification (silence = approval).theme/portabilityOne manifest, every target. Multi-target deploy, marketplace, packaging, install.type/bugSomething does not work as documented.

    Type

    No type

    Projects

    Status

    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions