Extension Attribute: Inventorying VS Code Extensions Per User
CompleteOverview
An extension attribute that runs VS Code’s own CLI as the logged-in user, lists every installed extension with its version, and reports the result into Jamf Pro inventory. It’s the verification half of the AI extension allowlist profile — the profile enforces, this proves it worked.
The problem
Standard inventory sees VS Code as one application at one version. That’s true and nearly useless, because almost nothing about a developer’s editor is determined by the editor. It’s determined by the extensions loaded into it — which run with the user’s privileges, read every file in the workspace, and frequently talk to the internet.
That’s a genuine supply-chain surface sitting entirely outside software inventory. And once you deploy an extension policy, you inherit a second problem: you can’t confirm a managed preference is doing anything. The profile is scoped, Jamf says it installed, and none of that tells you what’s actually loaded in the editor on that machine.
What it does
loggedInUser=$(echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }')
codePath="/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"
if [[ -e "${codePath}" ]]; then
result=$(sudo -u "${loggedInUser}" "${codePath}" --list-extensions --show-versions)
fi
The whole thing turns on one detail: extensions are per-user, and extension attributes run as root.
Running code --list-extensions as root enumerates root’s extension profile, which is empty on every machine in the fleet. You get a clean, confident, entirely wrong answer — the worst possible failure mode, because nothing errors and the field populates. sudo -u back to the console user is what makes the data real.
The rest is defensive detail that matters at fleet scale:
Three distinct states, not two. "Not installed" when VS Code isn’t there, "No extensions found" when it is but the user has none, and the actual list otherwise. Collapsing those into an empty string means a smart group can’t distinguish a developer running nothing unusual from a machine that never got the software.
--show-versions because the identifier alone is insufficient. Extension risk is version-specific — a compromised release matters, the extension in general usually doesn’t — and without a version you can’t answer “who is running the bad one.”
scutil for the console user, not whoami or $USER, which under a Jamf policy return root.
What it unlocks
Extension identifiers and versions become searchable inventory. When an extension turns out to be malicious — which happens on a regular cadence in every major marketplace — the question “which of our machines has it, at what version” is a search rather than an incident-response project.
It also closes the loop on the allowlist: if a denied extension shows up in inventory, the control isn’t working on that device, and you find out from a smart group instead of from an interview.
Outcome
Per-user IDE extension inventory across the Mac fleet, and independent verification that the extension policy is actually taking effect rather than merely being deployed.
Lessons Learned
Any script that inspects user-context state from a root-context management agent is wrong by default until it explicitly drops to the user. That class of bug doesn’t announce itself — it returns plausible, uniform, empty results across an entire fleet, and a field that reads “no extensions” everywhere looks like a finding rather than a mistake. The fix is one flag; noticing it’s needed is the actual skill.