Technical Reference · Microsoft Intune
Microsoft Intune
Deploy the device agent with Microsoft Intune: create the app, add the installer for each platform, deliver the identity file as a dependency, and connect the tenant to the dashboard for agent coverage.
This guide walks an Intune rollout of the device agent from start to finish: create the app, add the installer for each platform, deliver the identity file that enrolls each device without an interactive login, and connect the tenant to the dashboard so the platform can show which managed devices are covered. Shared concepts (the configuration schema, delivery channels, and coverage states) are on the MDM installations page.
Step 1: Set up the line-of-business app
Section titled “Step 1: Set up the line-of-business app”The agent is deployed as one Intune app per platform. In Apps → All apps → Add, choose the app type for each platform in the fleet:
| Platform | App type | Installer (Step 2) |
|---|---|---|
| macOS | Line-of-business app | speakeasy-agent_<version>.pkg |
| Windows | Windows app (Win32) | speakeasy-agent_<version>.msi, wrapped as .intunewin |
On Windows, use Windows app (Win32) rather than a line-of-business MSI. The identity file in Step 3 is attached as an app dependency, and Intune only supports dependencies between Win32 apps.
Settings that are the same regardless of platform:
- Name and Publisher: use the same name on both apps (for example Speakeasy Device Agent / Speakeasy) so the dependency picker in Step 3 and the Company Portal both show one obvious entry per platform.
- Assignments: assign the app as Required to the device group that should run the agent. Don’t assign the identity-file app from Step 3; as a dependency it installs wherever the agent app does.
Intune asks for the installer file on the first page of the wizard, so have the platform’s installer from Step 2 ready when creating each app.
Step 2: Add the installer for each platform
Section titled “Step 2: Add the installer for each platform”Windows
Section titled “Windows”-
In the dashboard, open Organization Settings → Secure → Device Agent, choose Windows under Install the agent, and download
speakeasy-agent_<version>.msi. -
Stage the MSI alone in an empty folder and package it with the Win32 Content Prep Tool (
IntuneWinAppUtil.exe):Terminal window IntuneWinAppUtil.exe -c <stage-folder> -s speakeasy-agent_<version>.msi -o out -
Upload the resulting
.intunewinas the app package file for the Windows app created in Step 1, then set the Program page:- Install command:
msiexec /i speakeasy-agent_<version>.msi /qn /norestart - Uninstall command:
msiexec /x speakeasy-agent_<version>.msi /qn /norestart - Install behavior: System
- Device restart behavior: No specific action
- Install command:
-
On the Detection rules page, match the rule to the selected update owner:
- For Intune-controlled updates, use an MSI or other version-aware rule for each release so the superseding app installs the newer version.
- For agent-controlled updates, use a version-independent File rule that checks for
C:\Program Files\Speakeasy\speakeasyd.exe. An MSI ProductCode rule can become false after an automatic MajorUpgrade and make Intune offer the older package again.
Choose one owner for Windows updates. Keep auto_update on "notify" when Intune controls version moves through a new .intunewin that supersedes the previous one. Use "automatic" when the agent should verify and install signed MSI updates. Do not schedule both update paths at the same time. See Windows reference.
- In the dashboard, open Organization Settings → Secure → Device Agent, choose macOS under Install the agent, and download
speakeasy-agent_<version>.pkg. - Upload it as the app package file for the macOS line-of-business app created in Step 1.
Detection needs no custom rule: Intune’s app model checks the pkg’s own receipt (com.speakeasy.agent.pkg).
Install it once. With auto_update: "automatic" in the identity file (Step 3), the agent keeps the daemon, CLI, and app current on its own. Re-push the pkg only for a change to the install layout itself, not for routine releases.
Intune can’t register the agent’s per-user systemd unit from a root script, so install on Linux through configuration management: follow the Linux guide, which covers the binaries, the unit, and the root helper package. An Intune Linux script can still place the speakeasyd and speakeasy binaries under /usr/local/bin if that helps.
Step 3: Set up the identity file
Section titled “Step 3: Set up the identity file”Every platform uses the same schema, but the update mode depends on the platform and update owner. Use automatic on macOS. On Windows, use notify for Intune-controlled updates or automatic for agent-controlled updates:
{ "v": 1, "email": "jane.doe@example.com", "org_token": "spk_org_…", "org_slug": "example-corp", "auto_update": "AUTO_UPDATE_MODE"}The file must be readable by the logged-in user, and the daemon only reads it at startup, so restart the daemon after any change.
Windows
Section titled “Windows”Package the script below as its own Win32 app and attach it to the agent app as a dependency.
1. Set the org token, update owner, and configuration revision. Save the following as Deploy-SpeakeasyManagedJson.ps1, replace REPLACE_WITH_ORG_TOKEN with the organization token from Organization Settings → Secure → Device Agent, and replace AUTO_UPDATE_MODE with notify for Intune-controlled updates or automatic for agent-controlled updates. Set CONFIG_REVISION to a unique value and change it whenever the token or configuration changes. Leave {{mail}} in place; the script substitutes it at install time.
$ErrorActionPreference = 'Stop'$configRevision = 'CONFIG_REVISION'
# {{mail}} is substituted below by this script — Intune does not expand# tokens inside scripts, only inside configuration profiles / app config.$template = @'{ "v": 1, "email": "{{mail}}", "org_token": "REPLACE_WITH_ORG_TOKEN", "org_slug": "example-corp", "org_name": "Example Corp", "auto_update": "AUTO_UPDATE_MODE"}'@
function Get-EnrolledUserEmail { # Primary: the UPN recorded at Intune enrollment (available in SYSTEM context, # even before any user has logged in) foreach ($e in Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -ErrorAction SilentlyContinue) { $upn = (Get-ItemProperty $e.PSPath -ErrorAction SilentlyContinue).UPN if ($upn) { return $upn } } # Fallback: the signed-in user's cached Entra identity $user = (Get-CimInstance Win32_ComputerSystem).UserName if ($user) { $sid = (New-Object System.Security.Principal.NTAccount($user)). Translate([System.Security.Principal.SecurityIdentifier]).Value $name = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\IdentityStore\Cache\$sid\IdentityCache\$sid" ` -ErrorAction SilentlyContinue).UserName if ($name -like '*@*') { return $name } } throw 'Could not determine the enrolled user''s email.'}
$email = Get-EnrolledUserEmail$json = $template.Replace('{{mail}}', $email)
$dir = Join-Path $env:ProgramData 'Speakeasy'$file = Join-Path $dir 'managed.json'
New-Item -ItemType Directory -Path $dir -Force | Out-Null# UTF-8 without BOM — a BOM can break strict JSON parsers[System.IO.File]::WriteAllText($file, $json, [System.Text.UTF8Encoding]::new($false))
# ACLs equivalent to dir 0755 / file 0640, using SIDs so it works on any OS language:# SYSTEM (S-1-5-18) + Administrators (S-1-5-32-544) full control, Users (S-1-5-32-545) read-only.# Inheritance is cut so ProgramData's default "users can create files" ACL does not apply.icacls $dir /inheritance:r /grant '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-32-545:(OI)(CI)RX' | Out-Nullicacls $file /inheritance:r /grant '*S-1-5-18:F' '*S-1-5-32-544:F' '*S-1-5-32-545:R' | Out-Null
# Reload a service that is already installed. On first deployment, the agent app starts it later.$daemon = Join-Path $env:ProgramFiles 'Speakeasy\speakeasyd.exe'if (Test-Path $daemon) { & $daemon -service restart }
# Write the revision marker last, after the configuration, ACL, and restart steps succeed.New-Item -ItemType File -Path (Join-Path $dir "managed-$configRevision.installed") -Force | Out-Null2. Package and upload the script as a Win32 app. Stage the script alone in an empty folder (the packer bundles the whole folder), then package it:
IntuneWinAppUtil.exe -c <stage-folder> -s Deploy-SpeakeasyManagedJson.ps1 -o outIn Apps → Windows → Add → Windows app (Win32), upload the .intunewin and set:
- Install command:
%windir%\sysnative\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Deploy-SpeakeasyManagedJson.ps1 - Uninstall command:
cmd.exe /c rmdir /s /q "%ProgramData%\Speakeasy" - Install behavior: System
- Detection rule: a File rule checking that
managed-CONFIG_REVISION.installedexists in%ProgramData%\Speakeasy. Use the same revision as the script. Intune uses detection rules to decide whether the app is already present, so checking only formanaged.jsonwould skip later configuration revisions. - Assignments: none. As a dependency it installs wherever the agent app does.
3. Add it as a dependency of the agent app. Open Apps → Windows → Speakeasy Device Agent → Properties → Dependencies → Add, select the config app, set Automatically install to Yes, and save.
Token rotation. Change CONFIG_REVISION, re-package the script as a new config app, and replace the old config app in the agent app’s dependency list. Do not rely on supersedence alone: Intune does not interchange a superseding Win32 app with an app dependency. The revision-specific detection rule causes Intune to run the new script, which restarts an existing service after writing the updated configuration.
Leave the icacls grants in the script as they are. The file must stay readable by Users, or enrollment silently fails.
Deliver the identity file as a custom configuration profile. One profile serves the whole fleet: Intune fills in {{mail}} per device.
-
Save the following as
speakeasy-agent.mobileconfig, replacing theorg_tokenandorg_slugvalues and generating fresh UUIDs (uuidgen) for bothPayloadUUIDkeys:<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>PayloadContent</key><array><dict><key>PayloadType</key><string>com.apple.ManagedClient.preferences</string><key>PayloadIdentifier</key><string>com.speakeasy.agent.preferences</string><key>PayloadUUID</key><string>REPLACE-WITH-UUID-1</string><key>PayloadVersion</key><integer>1</integer><key>PayloadContent</key><dict><key>com.speakeasy.agent</key><dict><key>Forced</key><array><dict><key>mcx_preference_settings</key><dict><key>v</key><integer>1</integer><key>email</key><string>{{mail}}</string><key>org_token</key><string>spk_org_…</string><key>org_slug</key><string>example-corp</string><key>auto_update</key><string>automatic</string></dict></dict></array></dict></dict></dict></array><key>PayloadDisplayName</key><string>Speakeasy device agent</string><key>PayloadIdentifier</key><string>com.speakeasy.agent.profile</string><key>PayloadScope</key><string>System</string><key>PayloadType</key><string>Configuration</string><key>PayloadUUID</key><string>REPLACE-WITH-UUID-2</string><key>PayloadVersion</key><integer>1</integer></dict></plist> -
In Devices → Configuration → Create → New policy, choose macOS and the Templates → Custom profile type, upload the file, and set Deployment channel to Device channel.
-
Assign it to the same group as the macOS app from Step 1.
-
Assign an idempotent macOS shell script to the same group and set Run script as signed-in user to No. Have it exit nonzero until both the managed preference and the agent are present, compare the preference file’s hash with a local last-loaded marker, and restart the console user’s daemon only when the hash changes:
launchctl kickstart -k "gui/$(id -u "$(stat -f%Su /dev/console)")/com.speakeasy.daemon". Record the marker only after a successful restart. The app and profile assignments have no documented delivery order. Configure a recurring Script frequency and retries so the script eventually reconciles initial delivery and later profile updates. Microsoft documents the available frequency and retry controls in Use shell scripts on macOS devices in Intune.
Keep {{mail}} lowercase: Intune doesn’t validate tokens, and {{Mail}} ships as a literal string. On a device, inspect the delivered profile with defaults read /Library/Managed\ Preferences/com.speakeasy.agent.
Fallback: a platform script. If the managed.json file path is required instead, use Devices → Scripts and remediations → Platform scripts → Add → macOS with a script that writes /Library/Application Support/Speakeasy/managed.json as root:wheel / 0644, then restarts the console user’s daemon. Three Intune-specific traps:
- Intune macOS scripts have no parameters and don’t expand
{{mail}}: the script has to resolve the email itself, and theorg_tokenlives in the script body. - Restart the daemon in the console user’s context, not as root:
launchctl kickstart -k "gui/$(id -u "$(stat -f%Su /dev/console)")/com.speakeasy.daemon". - Set Script frequency to a recurring interval rather than once, so rotated tokens reach devices.
Use an Intune Linux script to write /etc/speakeasy/managed.json as root:root / 0644, then follow the Linux guide to register and restart the per-user unit.
Verify on a device
Section titled “Verify on a device”speakeasy status on a target device should report the enrolled email with source: managed (surfaced in the menu bar UI as “Provisioned by IT”). If it reports source: local or source: none, the usual causes are file permissions that are too tight, the file at the wrong path, or the daemon not having restarted since the identity file was written. On Windows, the config app should also show as Installed in the agent app’s dependency status in Intune.
Step 4: Connect Intune to the dashboard
Section titled “Step 4: Connect Intune to the dashboard”The inventory pull runs through Microsoft Graph with a scope-limited app registration:
- In Entra ID → App registrations, create an app registration for the integration.
- Under API permissions, add the Microsoft Graph application permission
DeviceManagementManagedDevices.Read.All(reading the managed-device inventory is all the integration needs), and grant admin consent. - Under Certificates & secrets, create a client secret and copy its value.
- In the dashboard, open Organization Settings → Secure → Device Agent, switch to the MDM Integrations tab, and select Connect on the Microsoft Intune row.
- Enter the directory (tenant) ID, the application (client) ID, and the client secret. Credentials are stored encrypted and are never shown again after saving.
- Save, then Test connection. The test performs a real managed-devices read, validating the tenant ID, the credentials, and the admin-consented permission together.
- Enable the connection. New connections start paused: the flow is save, test, then enable.
Once enabled, the inventory syncs hourly. The integration reads each device’s Intune-recorded email address (falling back to the user principal name) to attribute devices to people. Coverage can only be attested for devices with an assigned user. The two lists to work during a rollout are No agent (deploy the agent to these users’ devices) and Agent stale (the agent was running and stopped; investigate whether it was disabled). The full set of coverage states is on the MDM installations page.
Intune has no supported inventory source for Linux devices in the dashboard, so Linux coverage is attested through Employee Enrollment rather than the fleet-wide managed-device view. The Intune inventory sync covers macOS and Windows devices.
Windows reference
Section titled “Windows reference”Details of the Windows install that are worth knowing but don’t change the steps above.
The service model differs from macOS. The daemon runs machine-wide as LocalSystem, not per-user, so standard users cannot stop it (better tamper resistance than the macOS Login Items toggle) while local admins can. Tool syncing still targets the active console user’s home (~/.claude, ~/.cursor, ~/.codex); with nobody signed in, the daemon idles rather than writing into the SYSTEM profile.
Version moves can be Intune-controlled or agent-controlled. For Intune-controlled updates, keep auto_update on "notify", package the newer MSI as a new .intunewin, add it as a new app version, or use Supersedence on the new app with Uninstall previous version set to No. For agent-controlled updates, use "automatic"; the daemon verifies the newer signed MSI and launches it through a detached Windows Installer process. The MSI’s MajorUpgrade stops the service, replaces the files, and starts it again without leaving an orphaned service entry. Do not schedule an Intune rollout at the same time as an automatic agent update. The MSI’s UpgradeCode is 368ec0a6-8862-4906-af34-8c3adb92c6e0, if inventory or supersedence tooling needs it.
The tray UI is installed but not started. The MSI lays down speakeasy-ui.exe without auto-starting it, matching the posture on every other OS. Deploy a Startup shortcut to C:\Program Files\Speakeasy\speakeasy-ui.exe if the fleet wants the tray icon. The daemon is fully functional without it.
Managed-layer enforcement needs nothing extra. Because the Windows daemon already runs as LocalSystem, it performs privileged writes in-process rather than through a separate root helper as macOS and Linux do. Setting a tool to "managed" lands its system configuration on the first reconcile, with no EnforcementPending window waiting on a package: %ProgramData%\OpenAI\Codex\requirements.toml (Codex), %ProgramData%\Cursor\hooks.json (Cursor), and %ProgramData%\ClaudeCode\managed-settings.json (Claude Code). Each is written SYSTEM-owned with a protected ACL (SYSTEM and Administrators full, Users read-only), the Windows equivalent of a root-owned 0644.
Daemon logs land in the SYSTEM profile when the agent runs as a service: C:\Windows\System32\config\systemprofile\AppData\Local\Speakeasy\Logs\. Run interactively, they go to %LOCALAPPDATA%\Speakeasy\Logs\ instead. Worth telling the support desk before they go hunting.
Scripted install without the MSI. Still valid for an air-gapped mirror or a custom install layout: resolve the version, URL, and SHA-256 from the release manifest (never hardcode them), place the binaries under C:\Program Files\Speakeasy\, then run speakeasyd -service install followed by -service start. Write managed.json with the same ACL grant as the script above, restart the service on every run, and move versions by stopping the service, replacing the binaries, and starting it again. The raw .zip binaries are not signed, so browser downloads of them may draw a SmartScreen warning; Intune line-of-business installs are unaffected.