-
Notifications
You must be signed in to change notification settings - Fork 4
Installation Checks
When managedsoftwareupdate processes an item from a manifest, it must decide whether the item needs to be installed, updated, or skipped. This document describes the full decision logic — the priority chain, per-type detection methods, and how each installer type is handled differently.
For each item in the manifest, Cimian walks through a set of checks in strict priority order. The first check that reaches a definitive answer wins — Cimian does not continue evaluating lower-priority checks once a result is returned.
The canonical source of this logic is StatusService.CheckStatus() in packages/CimianTools/cli/managedsoftwareupdate/Services/StatusService.cs.
Before anything else, if the item being evaluated is the CimianTools or Cimian package itself, Cimian compares the running binary version against the catalog version.
- Running version
>=catalog version → installed (skip) - Running version
<catalog version → fall through to normal checks
This prevents the running agent from mistakenly downgrading itself or triggering a redundant reinstall.
If the pkgsinfo contains an installcheck_script, it is executed. Exit codes are interpreted as predicates:
| Exit code | Meaning |
|---|---|
0 |
Install is needed — Cimian schedules the install |
| Non-zero | Install not needed — Cimian skips the item |
This check is authoritative. If
installcheck_scriptis defined, nothing below this point runs. This makes it appropriate for items that need custom detection logic — pref domains, registry keys, running processes, etc.
If the pkgsinfo defines an installs array, Cimian iterates each entry and verifies the item against the local system. If any entry fails verification, the item is marked as needing action. If all entries pass, the item is considered installed.
See Per-Type Breakdown: installs Array Entries below for how each entry type is evaluated.
If check.registry.name is defined, Cimian scans both the 64-bit and 32-bit views of HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall for an entry whose DisplayName contains the specified name (case-insensitive partial match).
| Outcome | Result |
|---|---|
| Not found | Install needed (new install) |
Found, no check.registry.version set |
Installed |
Found, installed DisplayVersion < catalog version |
Update needed |
Found, installed DisplayVersion >= catalog version |
Installed |
If check.file.path is defined, Cimian checks for the existence of that path.
| Outcome | Result |
|---|---|
| File not found | Install needed |
| File found, no version/hash checks | Installed |
File found, check.file.version set, installed < catalog |
Update needed |
File found, check.file.hash set, hash mismatch |
Update needed |
| All checks pass | Installed |
If check.script is defined, Cimian executes it. Exit codes follow the same predicate convention as installcheck_script:
| Exit code | Meaning |
|---|---|
0 |
Installed |
| Non-zero | Install needed |
If no explicit check (Priorities 1–5) is defined, Cimian looks up the item's name under HKLM\SOFTWARE\ManagedInstalls\<Name> — the registry key that Cimian itself writes after a successful installation.
This applies to all installer types (pkg, nupkg, copy, script, nopkg, msi, exe).
| Outcome | Result |
|---|---|
| No entry found | Fall through to Priority 7 |
Installed version < catalog version |
Update needed (UpdateAvailable) |
Installed version >= catalog version |
Installed |
This is the correct fallback for
pkgandnupkgitems that don't have a Windows MSI product code or a file to check against. Cimian's own install record is the source of truth for items it manages.
If every priority above produced no definitive answer and there is no ManagedInstalls registry receipt, Cimian marks the item as not installed and schedules it for installation.
Status: not-installed
NeedsAction: true
Reason: "No explicit checks defined and no installation receipt in registry"
ReasonCode: no_checks
Once Cimian successfully installs the item, it writes a receipt to HKLM\SOFTWARE\ManagedInstalls\<Name>. On subsequent runs, Priority 6 finds that receipt and handles version comparison — so the item will update correctly when a new catalog version appears.
This means a
pkgornupkgpkgsinfo with noinstallsarray and noinstallcheck_scriptwill install correctly on first run and track updates via the ManagedInstalls registry. No extra detection fields are required.
The installs array entries each have a type field that determines how they are verified.
Used by: pkg, nupkg, copy, exe, script, nopkg — any installer type
file exists?
├─ NO → NeedsAction=true (FileMissing)
└─ YES →
md5checksum defined?
├─ YES → hash matches?
│ ├─ NO → NeedsAction=true (HashMismatch)
│ └─ YES → hash is AUTHORITATIVE
│ version mismatch is informational only — item is accepted
└─ NO →
version defined? (falls back to item.version if not set on entry)
├─ YES → get file's PE/FileVersion resource
│ ├─ not readable + no hash → NeedsAction=true
│ ├─ file version < catalog → NeedsAction=true (VersionOutdated)
│ └─ file version >= catalog → installed
└─ NO → installed (file presence is sufficient)
Key behaviour: When a hash is provided and matches, version discrepancies are considered informational. The hash is the authority. This allows a file to report an internal version string that differs from the pkgsinfo version without triggering unnecessary reinstalls.
Used by: any installer type
directory exists?
├─ NO → NeedsAction=true (DirectoryMissing)
└─ YES → installed
No version checking is performed on directories.
Used by: msi installer type, or any type where an MSI product code is tracked in the installs array
CheckMsiWithUpgradeCode(product_code, upgrade_code, version)
│
├─ 1. ProductCode lookup
│ Scan HKLM\...\Uninstall\{ProductCode} (64-bit, then 32-bit)
│ found?
│ YES → version current/newer → installed
│ version outdated → NeedsAction=true
│
├─ 2. UpgradeCode lookup (handles Chrome-style auto-updaters)
│ Resolve UpgradeCode via HKLM\..\Installer\UpgradeCodes\{PackedGUID}
│ Cross-reference against Uninstall keys
│ found?
│ YES → version current/newer → installed
│ version outdated → NeedsAction=true
│
├─ 3. DisplayName fallback
│ Scan all Uninstall keys for exact DisplayName match
│ (uses item.display_name if set, otherwise item.name)
│ found?
│ YES → version compare → installed / NeedsAction=true
│
└─ 4. Not found anywhere → NeedsAction=true (ProductCodeMissing)
| Type | Built-in detection in installs[]
|
ManagedInstalls fallback (Priority 6) | Recommended detection |
|---|---|---|---|
| msi |
installs[type:msi] via ProductCode + UpgradeCode |
Yes | Always set product_code in pkgsinfo so installs[type:msi] works reliably |
| exe | None | Yes | Use installcheck_script, check.registry, or installs[type:file/msi] — EXE installers have no native detection |
| pkg / nupkg | None | Yes | ManagedInstalls registry is the correct fallback. No extra checks needed — first run installs, subsequent runs compare versions via Priority 6 |
| copy | None | Yes | Use installs[type:file] to verify the copied files |
| script | None | Yes | Use installcheck_script for stateless scripts that should always run |
| nopkg | None | Yes | If the item should run on every check, use installcheck_script that always exits 0 |
An installcheck_script that exits 0 (install needed) after the item has already been installed will cause Cimian to schedule an install every run. Ensure your postinstall_script puts the system into a state where installcheck_script returns non-zero on the next run.
Items that are designed to run every time (configuration enforcement, preference writes, etc.) should not rely on the ManagedInstalls fallback — Cimian will write an installation record after the first run, and subsequent runs will see the version as current and skip. Use installcheck_script to control execution explicitly.
In installs[type:file], if you provide both a hash and a version, the hash wins. If the hash matches, a version discrepancy will generate a warning in the log but will not trigger a reinstall. This is intentional: the hash proves the exact file is present.
-
ProductCode (
{GUID}) — unique per version. Changes with each release. -
UpgradeCode (
{GUID}) — stable across versions. Use this for apps like Chrome or Teams that silently update and change their ProductCode.
When both are present, Cimian tries ProductCode first (faster). It only falls back to UpgradeCode if the ProductCode is not found in the registry.
| Log message | Meaning |
|---|---|
CheckStatus explicitly indicates NO update required |
Item evaluated as installed |
CheckStatus explicitly indicates update required |
Item will be installed/updated |
No explicit checks defined - assuming installed |
Priority 7 fallback hit — no detection method found |
Checking status via installcheck_script |
Priority 1 running |
Checking installs array for file verification |
Priority 2 running |
Found MSI via ProductCode |
MSI detected via exact product code |
Found MSI via UpgradeCode |
MSI detected via upgrade code (version may differ from ProductCode) |
Found app via display_name fallback |
App found by name in Uninstall registry |
Registry version X < catalog version Y |
ManagedInstalls fallback detected an update is available |
Cimian — managed software deployment for Windows, modelled on Munki. Apache License 2.0. Source and issues at windowsadmins/cimian.
Start here
Repository
Authoring packages
- Installation Checks
- Uninstall Methods
- Uninstallable Key
- Conditional Items
- Importing EXE Bundle Installers
- PowerShell Execution Policy
- Chocolatey Shim Prevention
- Managed Profiles and Apps
Client runtime
Diagnostics
- Troubleshooting
- Logging
- Error Reporting
- Item Source Traceability
- CimiTrigger Troubleshooting
- Privilege Elevation
Integrations
Contributing