For IT teams and MSPs
Deploy Thoth to many computers.
Download, check the version and install silently for all users. Automatic updates are on by default, unless your organization has turned them off.
Get the script
Run in 64-bit Windows PowerShell 5.1 or later as SYSTEM or administrator, on Windows 10 or 11 x64. No parameters are needed for the latest stable release.
Deploy-Thoth.ps1
#Requires -Version 5.1
<#
Install or update Thoth for all users. Run as SYSTEM/admin in 64-bit PowerShell.
Default: latest stable release, automatic updates unless organization policy disables them.
Examples:
.\Deploy-Thoth.ps1
.\Deploy-Thoth.ps1 -CheckOnly
.\Deploy-Thoth.ps1 -UpdateMode Automatic
For a pinned release, supply InstallerUrl, Version and Sha512 together.
The installer must pass its SHA512 check and carry a valid Authenticode signature from ExpectedPublisher
(default: Spencer Heath) before it is run. AllowUnsigned skips only the signature check, for an internal
unsigned build, and says so.
Exit codes: 0 = success/current, 10 = update needed (CheckOnly),
20 = Thoth is open, 3010 = restart requested, 1 = error.
#>
[CmdletBinding()]
param
(
[ValidateSet('Managed', 'Automatic', 'Unchanged')]
[string]$UpdateMode = 'Automatic',
[switch]$CheckOnly,
[uri]$InstallerUrl,
[string]$Version,
[string]$Sha512,
[string]$ExpectedPublisher = 'Spencer Heath',
[switch]$AllowUnsigned
)
$ErrorActionPreference = 'Stop'
$UpdateModeSpecified = $PSBoundParameters.ContainsKey('UpdateMode')
$ManifestUrl = [uri]'https://downloads.usethoth.app/latest.yml'
$InstallerTimeoutSeconds = 900
function Convert-Version([string]$Value)
{
if ($Value -notmatch '^\d+\.\d+\.\d+(\.\d+)?$')
{
throw 'Expected a stable numeric version.'
}
$v = [version]$Value
return [version]::new($v.Major, $v.Minor, $v.Build, [Math]::Max(0, $v.Revision))
}
function Assert-Https([uri]$Url)
{
if (!$Url.IsAbsoluteUri -or $Url.Scheme -ne 'https' -or $Url.UserInfo -or $Url.Query -or $Url.Fragment)
{
throw 'Use a direct HTTPS URL without credentials, query strings or fragments.'
}
}
function Get-Release([uri]$Url)
{
Assert-Https $Url
$text = (Invoke-WebRequest -Uri $Url -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 60).Content
$fields = @{}
foreach ($line in ($text -split "`n"))
{
if ($line -match '^(version|path|sha512):\s*(.+?)\s*$')
{
if ($fields.ContainsKey($matches[1]))
{
throw 'Duplicate release field.'
}
$fields[$matches[1]] = $matches[2].Trim().Trim('"', "'")
}
}
foreach ($key in @('version', 'path', 'sha512'))
{
if (!$fields[$key])
{
throw "Release manifest is missing $key."
}
}
$download = [uri]::new($Url, [string]$fields.path)
Assert-Https $download
if ($download.Authority -ne $Url.Authority -or [IO.Path]::GetFileName($download.AbsolutePath) -ne "Thoth-Setup-$($fields.version).exe")
{
throw 'Unexpected installer URL in release manifest.'
}
return @{
Url = $download
Version = $fields.version
Hash = $fields.sha512
}
}
function Convert-Hash([string]$Value)
{
if ($Value -match '^[a-fA-F0-9]{128}$')
{
return $Value.ToUpperInvariant()
}
$bytes = [Convert]::FromBase64String($Value)
if ($bytes.Length -ne 64)
{
throw 'SHA512 must contain 64 bytes.'
}
return [BitConverter]::ToString($bytes).Replace('-', '')
}
function Assert-Signed([string]$Path)
{
$sig = Get-AuthenticodeSignature -LiteralPath $Path
$name = $null
if ($sig.SignerCertificate)
{
$name = $sig.SignerCertificate.GetNameInfo([Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false)
}
# Valid means the signature checks out and chains to a trusted root; the time stamp keeps it valid after the short-lived certificate expires.
if ($sig.Status -eq 'Valid' -and $name -eq $ExpectedPublisher -and $null -ne $sig.TimeStamperCertificate)
{
Write-Host "Installer is signed by $name."
return
}
if ($AllowUnsigned)
{
Write-Warning "The installer is not signed by $ExpectedPublisher (status: $($sig.Status), signer: $name). Continuing because -AllowUnsigned was given."
return
}
throw "The installer is not signed by $ExpectedPublisher (status: $($sig.Status), signer: $name). Installer was not run."
}
function Get-Installation
{
$paths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$entries = @(Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
Where-Object DisplayName -eq 'Thoth')
if ($entries.Count -gt 1)
{
throw 'Multiple machine installations found. Review them before deploying.'
}
if (!$entries.Count)
{
return $null
}
$entry = $entries[0]
if (!$entry.InstallLocation -or ![IO.Path]::IsPathRooted($entry.InstallLocation))
{
throw 'Thoth has an invalid install location.'
}
$exe = Join-Path $entry.InstallLocation 'Thoth.exe'
$installedVersion = $entry.DisplayVersion
$exists = Test-Path -LiteralPath $exe
if ($exists)
{
$info = [Diagnostics.FileVersionInfo]::GetVersionInfo($exe)
if ($info.ProductName -ne 'Thoth')
{
throw 'Unexpected product in Thoth install folder.'
}
$installedVersion = $info.ProductVersion
}
return @{
Version = Convert-Version $installedVersion
Exists = $exists
Msi = $entry.WindowsInstaller -eq 1
}
}
function Set-UpdatePolicy([string]$Mode)
{
if ($Mode -eq 'Unchanged' -or (!$UpdateModeSpecified -and $Mode -eq 'Automatic'))
{
return
}
$path = 'HKLM:\SOFTWARE\Policies\Thoth'
$value = [int]($Mode -eq 'Managed')
$null = New-Item -Path $path -Force
$null = New-ItemProperty -Path $path -Name DisableUpdates -PropertyType DWord -Value $value -Force
}
function Invoke-Deployment([hashtable]$Release)
{
# 1. Check the release and installed version. Never downgrade.
Assert-Https $Release.Url
$target = Convert-Version $Release.Version
$expectedHash = Convert-Hash $Release.Hash
$installed = Get-Installation
if ($installed -and $installed.Msi)
{
throw 'MSI installation detected. Upgrade it with your MSI deployment package.'
}
if ($installed -and $installed.Version -gt $target -and !$installed.Exists)
{
throw 'Newer installation needs repair. Refusing to downgrade.'
}
if ($installed -and $installed.Exists -and $installed.Version -ge $target)
{
Write-Host "Thoth $($installed.Version) is current or newer. Installer skipped."
if (!$CheckOnly)
{
Set-UpdatePolicy $UpdateMode
}
return 0
}
if ($CheckOnly)
{
Write-Host "Thoth $target is needed. No changes made."
return 10
}
if (Get-Process -Name Thoth -ErrorAction SilentlyContinue)
{
Write-Host 'Thoth is open. Close it and retry.'
return 20
}
# 2. Download into a private temporary folder and verify SHA512.
$stage = Join-Path ([IO.Path]::GetTempPath()) ('Thoth-Deploy-' + [guid]::NewGuid().ToString('N'))
$file = Join-Path $stage 'Thoth-Setup.exe'
$retain = $false
try
{
$null = New-Item -ItemType Directory -Path $stage
$acl = New-Object Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false)
foreach ($sid in @('S-1-5-18', 'S-1-5-32-544'))
{
$rule = New-Object Security.AccessControl.FileSystemAccessRule(
[Security.Principal.SecurityIdentifier]::new($sid),
'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'
)
$acl.AddAccessRule($rule)
}
Set-Acl -LiteralPath $stage -AclObject $acl
Write-Host "Downloading Thoth $target..."
Invoke-WebRequest -Uri $Release.Url -OutFile $file -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 300
if ((Get-FileHash -LiteralPath $file -Algorithm SHA512).Hash -ne $expectedHash)
{
throw 'Installer checksum mismatch. Installer was not run.'
}
Assert-Signed $file
# 3. Install silently for all users, then verify the installed version.
$process = Start-Process -FilePath $file -ArgumentList '/S /allusers' -WindowStyle Hidden -PassThru
$null = $process.Handle
if (!$process.WaitForExit($InstallerTimeoutSeconds * 1000))
{
$retain = $true
throw "Installer timed out and may still be running. Inspect the device before retrying. Staging: $stage"
}
$code = $process.ExitCode
if ($code -notin @(0, 3010))
{
throw "Installer failed with exit code $code."
}
for ($i = 0; $i -lt 12; $i++)
{
$after = Get-Installation
if ($after -and $after.Exists -and $after.Version -eq $target)
{
Set-UpdatePolicy $UpdateMode
Write-Host "Thoth $target installed and verified."
return $code
}
Start-Sleep -Seconds 5
}
throw 'Installer returned success, but the installed version could not be verified.'
}
finally
{
if (!$retain -and (Test-Path -LiteralPath $stage))
{
if (Test-Path -LiteralPath $file)
{
Remove-Item -LiteralPath $file -Force
}
if (!(Get-ChildItem -LiteralPath $stage -Force | Select-Object -First 1))
{
Remove-Item -LiteralPath $stage
}
}
}
}
try
{
if (![Environment]::Is64BitProcess -or ![Environment]::Is64BitOperatingSystem)
{
throw 'Run with 64-bit PowerShell on Windows x64.'
}
$principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent())
if (!$CheckOnly -and !$principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
{
throw 'Run as SYSTEM or an administrator.'
}
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
if ($InstallerUrl -or $Version -or $Sha512)
{
if (!$InstallerUrl -or !$Version -or !$Sha512)
{
throw 'Supply InstallerUrl, Version and Sha512 together, or leave all three unset for the latest release.'
}
$release = @{
Url = $InstallerUrl
Version = $Version
Hash = $Sha512
}
}
else
{
$release = Get-Release $ManifestUrl
}
exit (Invoke-Deployment $release)
}
catch
{
Write-Error -Message $_.Exception.Message -ErrorAction Continue
exit 1
}
Choose your deployment tool
NinjaOne setup
- In the Automation Library, add a Windows PowerShell script and paste the downloaded Deploy-Thoth.ps1 file.
- Select 64-bit architecture and run as System. Add optional parameters below when scheduling or running the script. Allow at least 25 minutes for the automation timeout.
- Run on one test device first. Read the activity output and exit code, open Thoth as a normal user, then expand to a small pilot group before scheduling a wider rollout.
No optional parameters needed. Automatic updates stay on unless an existing organization policy turns them off. Use -UpdateMode Managed only if your organization wants to deploy every future update itself.
Try one computer first
In an elevated 64-bit PowerShell window, open the downloaded script folder. Check without changing anything, then install. Follow your organization’s script execution policy.
.\Deploy-Thoth.ps1 -CheckOnly
.\Deploy-Thoth.ps1The script verifies SHA512 and that the installer is signed by Spencer Heath before running setup, skips current or newer versions, and waits if Thoth is open. It handles machine-wide EXE installs; existing MSI packages and per-user installs need a separate migration plan. Allow direct HTTPS access to downloads.usethoth.app.
Update preferences
Leave the parameters blank to keep automatic updates and respect any existing organization policy. Use an explicit option only when you want to change that policy. Reopen Thoth for the change to take effect.
-UpdateMode Managed
Turn off in-app updates for this computer. Your organization deploys future releases.
-UpdateMode Automatic
Explicitly re-enable in-app updates, including a computer previously configured as managed.
Optional: pin a specific release
For Intune or a staged rollout, supply these three values together. Replace every example value with your trusted release details. SHA512 accepts base64 or 128 hexadecimal characters. Direct HTTPS URLs cannot contain redirects, credentials or query strings. A pinned installer must be signed by the same publisher; for an internal unsigned build, add -AllowUnsigned (the script then warns and checks only the hash).
.\Deploy-Thoth.ps1 `
-InstallerUrl "https://YOUR-HOST/Thoth-Setup-1.2.3.exe" `
-Version "1.2.3" `
-Sha512 "RELEASE_SHA512"Read the result
- 0: Installed and verified, or already current/newer.
- 10: CheckOnly found an install or repair needed. No changes made.
- 20: Thoth is open. Ask the user to close it and retry.
- 3010: Installed; setup requested a restart. No automatic reboot.
- 1: Failed. Read the RMM activity output before retrying.
A missing manifest usually means the release is not published or the host is blocked. A checksum mismatch stops installation. Setup times out after 15 minutes and may still be running; inspect the device before retrying. Save the activity output with your deployment ticket.
On the pilot computer, check the machine uninstall entry named Thoth for the expected DisplayVersion and InstallLocation, then open Thoth as a normal user. File presence alone is not a version check.
After installation
Users can open Thoth and sign in with their own account. No credentials or license keys are included in the script. Existing guides and settings stay on the device.