MSI installer
PermitUSB ships as a single PermitUSB.msi. Per-machine install, signed, no forced reboot, clean uninstall. This page covers invoking msiexec directly - appropriate for Group Policy software installation, SCCM package commands, MDT task sequences, and any other scenario where the install is driven by an external system instead of an interactive admin.
For one-off installs and interactive upgrades on a single endpoint, use the PowerShell install script instead - it wraps msiexec with prerequisite checks, automatic .NET runtime install, and a purpose-built upgrade path that stops the running agent and tray cleanly.
Prerequisites
The installer blocks at launch with a clear message if either is missing. For fleet deploys, ensure both are in place before pushing the MSI.
- Windows 10, Windows 11, or Windows Server 2016 or later (x64). Older Windows lacks the WMI surface the agent uses for USB enumeration.
- .NET 10 Desktop Runtime (x64). The agent service and tray ship framework-dependent, so the runtime has to be present already. Get it from dot.net/download/dotnet/10.0 - pick the Windows Desktop Runtime installer (not the ASP.NET or SDK variants). For Intune / SCCM / GPO rollouts, the Microsoft Update Catalog distributes
.NET Desktop Runtime 10.0as a deployable package; chain it ahead of PermitUSB in your sequence.
Silent install
cmd.exe (the format every msiexec doc on the internet shows):
msiexec /i PermitUSB.msi /qn TENANT_TOKEN="<token>"PowerShell - use the --% stop-parsing token:
msiexec --% /i PermitUSB.msi /qn TENANT_TOKEN="<token>"ENDPOINT_GROUP="Information Technology" arrives at msiexec as two arguments and you get the usage dialog instead of an install. Workarounds, in order of preference:--%stop-parsing token (everything after is passed verbatim) - works on PS 5.1 and 7+.$PSNativeCommandArgumentPassing = 'Standard'in your$PROFILE- PS 7.3+ only, fixes the wart globally.- Run from
cmd.exe(or shell out viacmd /c msiexec …) - the original quoting works as documented.
Quote values that contain spaces - e.g. ENDPOINT_GROUP="Information Technology".
Properties
- TENANT_TOKEN - required for fresh installs. The enrollment token from the dashboard.
- SERVER - optional. The agent-channel URL. Defaults to
https://agent.permitusb.com. Lives on its own DNS-only subdomain (namedagent.rather thanapi.because it's for our installed software, not a public REST API) so traffic bypasses Cloudflare's edge buffering and bot-detection challenges; the dashboard atpermitusb.comstays Cloudflare-proxied. - ENDPOINT_GROUP - optional. Name of the endpoint group the new endpoint should join. Matched case-insensitively and trimmed of surrounding whitespace, so
"Information Technology"and"information technology"resolve to the same group. Unrecognized names fall back to the tenant default group with a warning logged on the cloud side. - PURGE_DATA - optional, only meaningful on uninstall. Set to
1to also remove%ProgramData%\PermitUSB\(config, event queue, credentials). Default uninstall leaves it behind so a reinstall picks up the same endpoint identity.
The dashboard's Enrollment page generates a token and shows the full msiexec command with the token pre-filled - no need to type it by hand. The Quickstart walks through it.
Logging
msiexec is silent on failure by default. Pass /l*v to write a verbose log somewhere you can find it:
msiexec /i PermitUSB.msi /qn TENANT_TOKEN="<token>" /l*v %TEMP%\PermitUSB-install.logCommon exit codes when something goes wrong:
1603- generic install failure (usually missing prereq or no admin)1618- another install is running, wait and retry1625- blocked by group policy3010- success but a reboot is requested (not required for the agent to function)
For 1603, search the log for "Return value 3" or "LaunchCondition" - the line just above is typically the actual cause.
Upgrading the agent
New version, same machines: run the new PermitUSB.msi with no properties.
msiexec /i PermitUSB.msi /qnWindows Installer matches the UpgradeCode across builds and runs a MajorUpgrade: stop service → uninstall old binaries → install new binaries → restart service. The agent picks up the new build on the next policy poll (~5 min by default) and the dashboard's "Agent version" column catches up via the X-Agent-Version drift header.
WixCloseApplications for the tray and ServiceControl Wait="yes" for the service, which usually works. A tray process that's hung or ignoring WM_CLOSE can hold the EXE handle and cause msiexec to bail with 1603. If you hit that, either use the PowerShell wrapper with -Upgrade (which taskkill /Fs the tray explicitly) or pre-stop the service and kill the tray yourself before running msiexec.What survives the upgrade. Everything in %ProgramData%\PermitUSB\ - agent.json, credentials.bin, events.db, policy.bin. Those are written by the agent at runtime, not by the MSI, so the installer's RemoveFiles never touches them. The endpoint stays enrolled, keeps its identity, keeps its group assignment, keeps its event queue.
What you don't need to pass on upgrade.
TENANT_TOKEN- already enrolled.credentials.binexists, the agent skips first-run enrollment entirely.ENDPOINT_GROUP-agent.jsonis canonical post-enrollment. The bootstrap registry isn't consulted again. Even if you pass it, it's ignored. To move groups post-install: dashboard, or editagent.jsonand restart the service.SERVER- same story;ApiUrlinagent.jsonwins.
Version-bump rule. The MSI's Version in Product.wxs must increase in one of its first three components or MajorUpgrade silently no-ops (Windows Installer ignores the fourth component when comparing). The C# binaries' assembly version is set in agent/Directory.Build.props and feeds the dashboard's "Agent version" column - bump both files together.
Edge cases.
- Migrating an enrolled endpoint to a new API host.
agent.json'sApiUrlis set during first-run enrollment and stays put across MSI upgrades - the installer doesn't rewrite the file. So if you change the cloud-side API hostname (e.g. moving frompermitusb.comtoagent.permitusb.com), an existing endpoint keeps talking to the old host. Two ways to migrate: (a) hand-edit%ProgramData%\PermitUSB\agent.jsonto the newApiUrland restart the service, or (b) runReset-PermitUSB.ps1+ reinstall the MSI (which picks up the new defaultSERVERautomatically). New installs always go to the installer's current default, no migration needed. - Downgrade is blocked. WiX's MajorUpgrade refuses to install an older version over a newer one. Uninstall first if you really need to roll back.
- Force a re-enroll (tenant move, stuck agent): stop the service, delete
credentials.binandagent.jsonfrom%ProgramData%\PermitUSB\, then reinstall with a freshTENANT_TOKEN.
Moving an endpoint between groups after install
Two equally good ways:
- From the dashboard's Endpoints page. The agent picks up the change on its next policy poll (within
PolicyPollIntervalSeconds, default 5 min) and updates its localagent.jsonto match. - On the endpoint, edit
%ProgramData%\PermitUSB\agent.jsonand setEndpointGroupto the new group's name, then restart thePermitUSB.Agentservice. Useful for site-side admins re-homing a machine without dashboard access. Local edits win on service restart; mid-session moves come from the dashboard.
What gets installed
%ProgramFiles%\PermitUSB\Agent\- the Worker Service%ProgramFiles%\PermitUSB\Tray\- the per-user tray app%ProgramData%\PermitUSB\- config, SQLite event store, encrypted credentials- Service:
PermitUSB.Agent, starts automatically - Run-key:
HKLM\Software\Microsoft\Windows\CurrentVersion\Run\PermitUSB.Tray - Bootstrap registry:
HKLM\Software\PermitUSB\Bootstrap(cleared on uninstall)
Code signing
Production builds are EV code-signed. The service, tray app, bundled PermitUSB libraries, and the MSI package each carry a SHA-256 Authenticode signature from JJMK Studios, LLC (the company behind PermitUSB), so Windows shows a verified publisher and Defender SmartScreen does not warn on install. Local pre-release builds may be self-signed; SmartScreen warns on those, and that warning is expected until an EV-signed build replaces them.
Uninstall
msiexec /x PermitUSB.msi /qnRemoves service, tray app, files, registry keys. Leaves the tenant + endpoint records on the cloud - delete those from the dashboard's Endpoints page. To also remove %ProgramData%\PermitUSB\, pass PURGE_DATA=1:
msiexec /x PermitUSB.msi /qn PURGE_DATA=1