Extension Attribute: Inventorying VS Code Extensions Per User

Complete
Author July 2026

Overview

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 is 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 is true and nearly useless, because almost nothing about a developer’s editor is determined by the editor. It is 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 is a real supply-chain surface sitting entirely outside software inventory. And once you deploy an extension policy you inherit a second problem: you cannot confirm a managed preference is doing anything. The profile is scoped, Jamf says it installed, and none of that tells you what is 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 installed" when VS Code is not there, "No extensions found" when it is but the user has none, and the list otherwise. Collapsing those into an empty string means a smart group cannot 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 does not, and without a version you cannot answer “who is running the bad one”.

scutil finds the console user. whoami and $USER return root under a Jamf policy.

What it unlocks

Extension identifiers and versions become searchable inventory. When an extension turns out to be malicious, which happens regularly in every major marketplace, the question “which of our machines has it, at what version” is a search and not an incident-response project.

It also closes the loop on the allowlist. If a denied extension shows up in inventory, the control is not 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 taking effect instead of 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 does not announce itself. It returns plausible, uniform, empty results across an entire fleet, and a field that reads “no extensions” everywhere looks like a finding instead of a mistake. The fix is one flag. Noticing it is needed is the skill.