No deployment platform, twenty machines, one MSI. PsExec will deploy MSI packages across a list of hosts in a single command — the pieces that need care are where the file lives and how you read the results.

Prepare the list and the share

Download PsTools from Sysinternals and create a text file with one IP address or hostname per line — D:\MSI_Deploy\hosts.txt:

192.168.20.101
192.168.20.102
192.168.20.103
192.168.20.104

Put the MSI on a share with read and execute permissions, for example \\192.168.20.100\Kits\setup.msi.

Error 1619: this installation package could not be opened

This is where the job usually fails, and it is worth understanding rather than copying. PsExec starts msiexec on the remote machine, and that process then reaches back across the network for the MSI — a second hop, to a third machine. Which identity arrives at the share depends entirely on how you launched it:

  • No -u at all. Sysinternals is explicit: "the process will run in the context of your account on the remote system, but will not have access to network resources (because it is impersonating)". The share sees an anonymous connection, and Everyone does not help — the Anonymous Logon SID has not been part of Everyone since Windows XP SP2.
  • -s. The process runs as SYSTEM and reaches the share as the computer account, EXAMPLE\HOST$. This is the case where granting read to Domain Computers is the right fix, and it is what most PsExec recipes online quietly rely on.
  • -u DOMAIN\user with a password. This is the documented way out — the same page says to "specify a valid user name in the Domain\User syntax if the remote process requires access to network resources". The remote msiexec authenticates as that user, so that user needs read on the share and on the NTFS folder underneath.

The trap in the third case is passing a local administrator account. The file server does not recognise it, the connection falls back to anonymous, and every host returns 1619 while the command itself looks perfectly healthy.

Deploy MSI to every host in the list

PsExec @D:\MSI_Deploy\hosts.txt -u <username> msiexec.exe /i "\\192.168.20.100\Kits\setup.msi" /q
\\192.168.20.101:
msiexec.exe exited on 192.168.20.101 with error code 0.
\\192.168.20.102:
msiexec.exe exited on 192.168.20.102 with error code 0.
\\192.168.20.103:
msiexec.exe exited on 192.168.20.103 with error code 0.
\\192.168.20.104:
msiexec.exe exited on 192.168.20.104 with error code 0.

Two switches worth adding to that command line in practice:

  • /norestart — otherwise a package that wants a reboot may take it, on twenty machines, while people are working on them.
  • /l*v C:\Windows\Temp\install.log — verbose logging on the target. When one host out of twenty fails, this is the only thing that tells you why.

And three PsExec switches that decide whether the run finishes at all: -n 10 sets a connection timeout, without which one powered-off machine stalls the whole list; -accepteula -nobanner keeps scripted output clean; and -h runs with the account's elevated token, without which a per-machine install can come back 1603 on a UAC-filtered logon.

If the share is awkward, stage the MSI locally instead. -c will not do it: the documentation says it copies "the specified executable", which here is msiexec.exe — already on every target — and the MSI would still come over the network. Copy the file from your own session, which is a single hop, then install from the local path:

for /f %h in (D:\MSI_Deploy\hosts.txt) do copy /y D:\MSI_Deploy\setup.msi \\%h\C$\Windows\Temp\
PsExec @D:\MSI_Deploy\hosts.txt -u <username> msiexec.exe /i C:\Windows\Temp\setup.msi /q /norestart

That removes the second hop entirely, and is the cheapest way to deploy MSI packages when the share is the problem.

Read the exit codes, do not skim them

error code 0 is success. The two others you will actually see:

  • 3010 — success, but a reboot is required to finish. This is not a failure, and treating it as one is how people re-run a deployment that already worked.
  • 1603 — fatal error during installation. The generic one. It means go and read the log you hopefully enabled, because it covers everything from "already installed" to "no disk space".

Also 1618 (another installation is already in progress — the _MSIExecute mutex is held by something else at that moment), 1619 (package could not be opened — see the share permissions above), 1620 (the file was readable but is not a valid MSI — the opposite conclusion from 1619, which is why they are worth telling apart) and 1612 (the installation source is no longer available). The full list is on Microsoft's MsiExec error codes page.

PsExec itself is also routinely blocked by Defender and other endpoint products as a lateral-movement tool. A run that dies with nothing in any log is usually that, not your command line.

Verify

Do not trust the exit codes alone. Check the product is actually registered:

Invoke-Command -ComputerName (Get-Content D:\MSI_Deploy\hosts.txt) -ScriptBlock {
    Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*,
                     HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |
        Where-Object DisplayName -like "*YourProduct*" |
        Select-Object DisplayName, DisplayVersion
}

Read the registry rather than Win32_Product — querying that WMI class starts a consistency check that verifies and repairs every installed MSI on the machine, logging an Event 1035 per product. It is slow, and a repair can revert settings.

Two prerequisites for that snippet, since the rest of the post only needs SMB: it uses PowerShell remoting, and because hosts.txt holds IP addresses rather than names those addresses have to be in TrustedHosts. The PsExec-native equivalent avoids both:

PsExec @D:\MSI_Deploy\hosts.txt -u <username> cmd /c "reg query HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall /s /f YourProduct"

Taking it back off

PsExec @D:\MSI_Deploy\hosts.txt -u <username> msiexec.exe /x {ProductCode} /qn /norestart

The {ProductCode} is the registry key name under Uninstall that the verification step above just listed.

When this is the right tool

For a one-off push to a known list, it is hard to beat: you deploy MSI packages to exactly the machines you named, and you see the result of each one. For anything recurring, the exit codes have to be collected and acted on, failures have to be retried, and that is what deployment tooling exists for — Intune, a GPO software installation policy, or whatever you already run. Use this to get out of a hole, not as a process.

The same @file host-list pattern drives updating DNS servers across a fleet, and the PsExec requirements on the target — admin share, firewall, the account you connect with — are the same ones covered in creating a user on a remote workstation.

Leave a Reply