> For the complete documentation index, see [llms.txt](https://docs.we360.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.we360.ai/deployment-and-it-ops/deployment/agent-deployment-hub/manual-installation/windows-validation.md).

# Windows Installation Validation

Verify We360.ai MyZen agent installation on Windows — check processes, services, and validate endpoint monitoring is active.

## Automated Verification Script (Stealth)

For scripted / bulk validation of a **stealth** install, use `zs-postverify.ps1`. It is brand-agnostic (it discovers the install directory rather than hardcoding it, so it works whichever branded build is installed) and checks everything a manual pass would, in one run:

* The `svcmonitor` service exists **and** is `Running` — with an initial wait plus two retries (\~30s apart), since the service can take a moment to reach `Running` right after install.
* `svcrunner` is registered as `svcmonitor`'s recovery (failure) action — it is **not** a service of its own, so Windows starts it when `svcmonitor` fails. Whether it happens to be running is advisory only.
* Service binaries `C:\Windows\svcmonitor.exe` and `C:\Windows\svcrunner.exe`.
* The `MyZenV2s.exe` agent and its companions (`zen_cli.exe`, `cleanup_mgr.exe`, `version.txt`, `ffmpeg`).
* The tenant keyconfig (`C:\Windows\System32\zs.json`, or the in-tree fallback).
* The Visual C++ runtime files and minimum version.
* Whether the agent process is currently running.

Each check prints `[ PASS ]` / `[ FAIL ]` / `[ WARN ]` (WARN is advisory and never fails the run). The script **exits `0`** when all required checks pass, **`1`** otherwise — suitable for use as a post-install gate in RMM / MDM tooling. Add `-Json` for a machine-readable summary.

{% hint style="info" %}
The agent is 64-bit, so its files live in the real `C:\Windows\System32`. If your RMM / MDM tool runs the script from **32-bit** PowerShell, Windows silently redirects `System32` to `SysWOW64` — the script resolves the real path itself, so it reports correctly either way. No action needed on your side.
{% endhint %}

Save the script below as `zs-postverify.ps1` and run it from an **elevated** PowerShell prompt:

```powershell
powershell -ExecutionPolicy Bypass -File .\zs-postverify.ps1
```

To skip the initial wait for a quick manual check (e.g. long after install):

```powershell
powershell -ExecutionPolicy Bypass -File .\zs-postverify.ps1 -SvcMonitorInitialWaitSeconds 0 -SvcMonitorRetries 0
```

<details>

<summary>zs-postverify.ps1</summary>

```powershell
<#
.SYNOPSIS
    Post-install verification for the ZS stealth desktop agent on Windows.

.DESCRIPTION
    Verifies that a completed stealth ("zs") install is present and healthy.
    Brand-agnostic: it does NOT assume a particular branded
    install directory -- the shared, fixed pieces (the svcmonitor / svcrunner
    services under C:\Windows, the MyZenV2s.exe agent, the VC runtime, the
    keyconfig) are what an install is judged by, and the brand only changes the
    Program Files subdirectory, which is discovered rather than hardcoded.

    Every check prints [ PASS ] / [ FAIL ] / [ WARN ] / [ INFO ]. WARN checks
    are advisory (state that is legitimately variable, e.g. svcrunner can be
    Stopped by design once it has started the agent) and never fail the run.
    The script exits 0 when all required checks pass, 1 otherwise.

.PARAMETER Json
    Emit a machine-readable JSON summary to stdout instead of the human report.

.PARAMETER MinVcRuntimeVersion
    Minimum acceptable VC++ runtime version (msvcp140.dll). Defaults to the
    version the agent is built against.

.EXAMPLE
    powershell -ExecutionPolicy Bypass -File .\zs-postverify.ps1

.EXAMPLE
    powershell -ExecutionPolicy Bypass -File .\zs-postverify.ps1 -Json

.NOTES
    Run elevated (as Administrator) for complete results -- service state and the
    ProgramData install tree are not fully readable to a standard user.
#>

[CmdletBinding()]
param(
    [switch]$Json,
    [string]$MinVcRuntimeVersion = "14.42.34438",

    # svcmonitor can take a moment to reach Running right after install (AV
    # settle, service auto-start). We wait once, then re-check on an interval.
    [int]$SvcMonitorInitialWaitSeconds = 30,
    [int]$SvcMonitorRetryIntervalSeconds = 30,
    [int]$SvcMonitorRetries = 2
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

# ---------------------------------------------------------------------------
# Canonical, brand-agnostic install facts (mirrors pkg/apps/stealth/settings
# and pkg/system/os_windows.go in the desktop-app repo).
# ---------------------------------------------------------------------------
$SvcMonitorName   = "svcmonitor"
$SvcRunnerName    = "svcrunner"
$SvcMonitorBinary = "C:\Windows\svcmonitor.exe"
$SvcRunnerBinary  = "C:\Windows\svcrunner.exe"

# The stealth agent binary. Same file name across brands -- only its parent
# directory under "C:\Program Files\<brand>\zs" differs, so we discover it.
$AgentExeName     = "MyZenV2s.exe"

# Companion files expected alongside the agent in the install directory.
$CompanionFiles   = @("zen_cli.exe", "cleanup_mgr.exe", "version.txt")

# Under a 32-bit host process (plenty of RMM/MDM agents spawn 32-bit
# PowerShell) the WOW64 file-system redirector silently maps C:\Windows\System32
# to C:\Windows\SysWOW64, where the 64-bit agent's DLLs and config do not live.
# "Sysnative" is the redirector's escape hatch back to the real System32.
$System32 = if (-not [Environment]::Is64BitProcess -and
                [Environment]::Is64BitOperatingSystem) {
    "C:\Windows\Sysnative"
} else {
    "C:\Windows\System32"
}

# Keyconfig (tenant config) -- primary path plus the in-tree fallback.
$KeyConfigPrimary = Join-Path $System32 "zs.json"

# VC++ redistributable runtime the C++ agent links against.
$VcRuntimeFiles   = @((Join-Path $System32 "vcruntime140_1.dll"),
                      (Join-Path $System32 "msvcp140.dll"))
$VcVersionFile    = Join-Path $System32 "msvcp140.dll"

# ---------------------------------------------------------------------------
# Result plumbing
# ---------------------------------------------------------------------------
$script:Results = New-Object System.Collections.Generic.List[object]

function Add-Result {
    param(
        [ValidateSet("PASS", "FAIL", "WARN", "INFO")][string]$Status,
        [string]$Name,
        [string]$Detail
    )
    $script:Results.Add([pscustomobject]@{
        Status = $Status
        Name   = $Name
        Detail = $Detail
    })
    if (-not $Json) {
        $color = switch ($Status) {
            "PASS" { "Green" }
            "FAIL" { "Red" }
            "WARN" { "Yellow" }
            default { "Cyan" }
        }
        Write-Host ("[ {0,-4} ] " -f $Status) -ForegroundColor $color -NoNewline
        Write-Host ("{0,-34} {1}" -f $Name, $Detail)
    }
}

function Test-IsAdmin {
    try {
        $id = [Security.Principal.WindowsIdentity]::GetCurrent()
        return ([Security.Principal.WindowsPrincipal]$id).IsInRole(
            [Security.Principal.WindowsBuiltInRole]::Administrator)
    } catch { return $false }
}

# ---------------------------------------------------------------------------
# Checks
# ---------------------------------------------------------------------------
if (-not $Json) {
    Write-Host ""
    Write-Host "ZS stealth install verification" -ForegroundColor White
    Write-Host "===============================" -ForegroundColor White
    Write-Host ("Host: {0}   Time: {1}" -f $env:COMPUTERNAME, (Get-Date -Format "yyyy-MM-dd HH:mm:ss"))
    Write-Host ""
}

# 0. Elevation (advisory -- some checks degrade without it)
if (Test-IsAdmin) {
    Add-Result PASS "Elevation" "running as Administrator"
} else {
    Add-Result WARN "Elevation" "not elevated; service/ProgramData results may be incomplete"
}

# 1. svcmonitor service -- must exist AND be running. Give it time to come up:
#    an initial wait, then re-check every interval up to $SvcMonitorRetries.
function Get-SvcMonitor { Get-Service -Name $SvcMonitorName -ErrorAction SilentlyContinue }

if (-not $Json -and $SvcMonitorInitialWaitSeconds -gt 0) {
    Write-Host ("[ INFO ] Waiting {0}s for {1} to settle..." -f $SvcMonitorInitialWaitSeconds, $SvcMonitorName) -ForegroundColor Cyan
}
if ($SvcMonitorInitialWaitSeconds -gt 0) { Start-Sleep -Seconds $SvcMonitorInitialWaitSeconds }

$svcMon = Get-SvcMonitor
$attempt = 0
while (($null -eq $svcMon -or $svcMon.Status -ne "Running") -and $attempt -lt $SvcMonitorRetries) {
    $attempt++
    if (-not $Json) {
        $seen = if ($null -eq $svcMon) { "not installed" } else { $svcMon.Status }
        Write-Host ("[ INFO ] {0} {1}; retry {2}/{3} in {4}s..." -f `
            $SvcMonitorName, $seen, $attempt, $SvcMonitorRetries, $SvcMonitorRetryIntervalSeconds) -ForegroundColor Cyan
    }
    Start-Sleep -Seconds $SvcMonitorRetryIntervalSeconds
    $svcMon = Get-SvcMonitor
}

if ($null -eq $svcMon) {
    Add-Result FAIL "Service: $SvcMonitorName" ("not installed (after {0} retries)" -f $SvcMonitorRetries)
} elseif ($svcMon.Status -eq "Running") {
    $suffix = if ($attempt -gt 0) { " (after retry $attempt)" } else { "" }
    Add-Result PASS "Service: $SvcMonitorName" ("installed and Running{0}" -f $suffix)
} else {
    Add-Result FAIL "Service: $SvcMonitorName" ("installed but {0} (expected Running, after {1} retries)" -f $svcMon.Status, $SvcMonitorRetries)
}

# 2. svcrunner is NOT a service of its own. It is registered as svcmonitor's
#    recovery (failure-action) command, so Windows launches it when svcmonitor
#    fails. What must be true is that svcmonitor's FailureCommand points at it.
$failCmd = (Get-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Services\$SvcMonitorName" `
                -Name FailureCommand -ErrorAction SilentlyContinue).FailureCommand
if ($failCmd -and $failCmd -match [regex]::Escape($SvcRunnerName)) {
    Add-Result PASS "Recovery: $SvcRunnerName" "registered as $SvcMonitorName failure action"
} else {
    Add-Result FAIL "Recovery: $SvcRunnerName" ("not registered as $SvcMonitorName failure action (FailureCommand = '{0}')" -f $failCmd)
}

#    Whether it is currently running is advisory: it is started on demand and
#    exits once it has done its work.
$runProc = Get-Process -Name $SvcRunnerName -ErrorAction SilentlyContinue
if ($runProc) {
    Add-Result PASS "Process: $SvcRunnerName" ("running (PID {0})" -f ($runProc | Select-Object -First 1).Id)
} else {
    Add-Result WARN "Process: $SvcRunnerName" "not currently running (starts on demand)"
}

# 3. Service binaries on disk (fixed paths, shared across brands).
foreach ($bin in @($SvcMonitorBinary, $SvcRunnerBinary)) {
    if (Test-Path -LiteralPath $bin) {
        Add-Result PASS "File: $(Split-Path $bin -Leaf)" $bin
    } else {
        Add-Result FAIL "File: $(Split-Path $bin -Leaf)" "missing: $bin"
    }
}

# 4. Locate the agent (MyZenV2s.exe) without assuming the brand directory.
#    Prefer the canonical "<brand>\zs" layout, then fall back to a scan.
$agentPath = $null
$programDirs = @("C:\Program Files", "C:\Program Files (x86)") |
    Where-Object { Test-Path -LiteralPath $_ }

foreach ($root in $programDirs) {
    $candidate = Get-ChildItem -LiteralPath $root -Directory -ErrorAction SilentlyContinue |
        ForEach-Object { Join-Path $_.FullName "zs\$AgentExeName" } |
        Where-Object { Test-Path -LiteralPath $_ } |
        Select-Object -First 1
    if ($candidate) { $agentPath = $candidate; break }
}
if (-not $agentPath) {
    foreach ($root in $programDirs) {
        $hit = Get-ChildItem -LiteralPath $root -Recurse -Filter $AgentExeName `
                   -ErrorAction SilentlyContinue -File -Depth 4 | Select-Object -First 1
        if ($hit) { $agentPath = $hit.FullName; break }
    }
}

$installDir = $null
if ($agentPath) {
    $installDir = Split-Path -Parent $agentPath
    Add-Result PASS "File: $AgentExeName" $agentPath
} else {
    Add-Result FAIL "File: $AgentExeName" "not found under Program Files"
}

# 5. Companion files in the install directory.
if ($installDir) {
    foreach ($f in $CompanionFiles) {
        $p = Join-Path $installDir $f
        if (Test-Path -LiteralPath $p) {
            $extra = ""
            if ($f -eq "version.txt") {
                try { $extra = "= " + ((Get-Content -LiteralPath $p -Raw).Trim()) } catch {}
            }
            Add-Result PASS "File: $f" ("{0} {1}" -f $p, $extra).Trim()
        } else {
            # version.txt absence is advisory; the binaries are required.
            if ($f -eq "version.txt") {
                Add-Result WARN "File: $f" "missing (agent may not have written it yet)"
            } else {
                Add-Result FAIL "File: $f" "missing: $p"
            }
        }
    }

    # ffmpeg is the screen-recording backend; usually bundled in the install dir.
    $ffmpeg = Get-ChildItem -LiteralPath $installDir -Filter "ffmpeg*.exe" `
                  -ErrorAction SilentlyContinue -File | Select-Object -First 1
    if ($ffmpeg) {
        Add-Result PASS "File: ffmpeg" $ffmpeg.FullName
    } else {
        Add-Result WARN "File: ffmpeg" "not found in install dir (recording backend)"
    }
} else {
    Add-Result WARN "Companion files" "skipped -- install directory unknown"
}

# 6. Keyconfig / tenant config.
$keyConfigFallback = if ($installDir) { Join-Path $installDir "keyconfig.json" } else { $null }
$keyConfigFound = $null
if (Test-Path -LiteralPath $KeyConfigPrimary) {
    $keyConfigFound = $KeyConfigPrimary
} elseif ($keyConfigFallback -and (Test-Path -LiteralPath $keyConfigFallback)) {
    $keyConfigFound = $keyConfigFallback
}
if ($keyConfigFound) {
    $tenant = ""
    try {
        $cfg = Get-Content -LiteralPath $keyConfigFound -Raw | ConvertFrom-Json
        if ($cfg.PSObject.Properties.Name -contains "tenant_id" -and $cfg.tenant_id) {
            $tenant = "tenant_id=$($cfg.tenant_id)"
        }
        Add-Result PASS "Keyconfig" ("{0} {1}" -f $keyConfigFound, $tenant).Trim()
    } catch {
        Add-Result FAIL "Keyconfig" "$keyConfigFound present but not valid JSON"
    }
} else {
    Add-Result FAIL "Keyconfig" "not found (checked $KeyConfigPrimary and install dir)"
}

# 7. VC++ runtime files + version floor.
$vcFilesOk = $true
foreach ($f in $VcRuntimeFiles) {
    if (-not (Test-Path -LiteralPath $f)) {
        $vcFilesOk = $false
        Add-Result FAIL "VC runtime" "missing: $f"
    }
}
if ($vcFilesOk) {
    try {
        $vcVer = (Get-Item -LiteralPath $VcVersionFile).VersionInfo.ProductVersion
        if (-not $vcVer) { $vcVer = (Get-Item -LiteralPath $VcVersionFile).VersionInfo.FileVersion }
        $verClean = ($vcVer -split '[^0-9\.]')[0]
        if ($verClean -and ([version]$verClean -ge [version]$MinVcRuntimeVersion)) {
            Add-Result PASS "VC runtime" "$verClean (>= $MinVcRuntimeVersion)"
        } else {
            Add-Result FAIL "VC runtime" "$verClean (< required $MinVcRuntimeVersion)"
        }
    } catch {
        Add-Result WARN "VC runtime" "files present but version unreadable"
    }
}

# 8. Agent process running (advisory -- svcmonitor will (re)start it, and it may
#    be mid-restart at the moment of check).
$proc = Get-Process -Name ([IO.Path]::GetFileNameWithoutExtension($AgentExeName)) `
            -ErrorAction SilentlyContinue
if ($proc) {
    Add-Result PASS "Process: $AgentExeName" ("running (PID {0})" -f ($proc | Select-Object -First 1).Id)
} else {
    Add-Result WARN "Process: $AgentExeName" "not currently running (svcmonitor should start it)"
}

# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
$fail = @($script:Results | Where-Object { $_.Status -eq "FAIL" }).Count
$warn = @($script:Results | Where-Object { $_.Status -eq "WARN" }).Count
$pass = @($script:Results | Where-Object { $_.Status -eq "PASS" }).Count
$ok   = ($fail -eq 0)

if ($Json) {
    [pscustomobject]@{
        ok         = $ok
        pass       = $pass
        warn       = $warn
        fail       = $fail
        installDir = $installDir
        agentPath  = $agentPath
        keyConfig  = $keyConfigFound
        checks     = $script:Results
    } | ConvertTo-Json -Depth 5
} else {
    Write-Host ""
    Write-Host ("Summary: {0} passed, {1} warnings, {2} failed" -f $pass, $warn, $fail) `
        -ForegroundColor $(if ($ok) { "Green" } else { "Red" })
    if ($ok) {
        Write-Host "RESULT: Install verified." -ForegroundColor Green
    } else {
        Write-Host "RESULT: Install verification FAILED." -ForegroundColor Red
    }
    Write-Host ""
}

exit $(if ($ok) { 0 } else { 1 })
```

</details>

## Check Running Status

### Standard Mode

1. Open **Task Manager**.
2. Search for `MyZenV2` in the process list.
3. If multiple instances appear, right-click each and select **Go to details**.
4. Verify the **User name** column matches the currently logged-in user.

### Stealth Mode

1. Open **Task Manager**.
2. Search for `MyZenV2s` in the process list. Alternatively search for `zs` in the process list.
3. Right-click and select **Go to details**. Verify the **User name** matches the current user.
4. Additionally, confirm that the `svcmonitor` process is running.

## Check Application Version

The **installer version** is the authoritative version indicator. The version shown in the MyZen app window may differ.

### Standard Mode

**Option 1:** Read the contents of:

```
C:\Program Files\Zenstack\MyZenV2\version.txt
```

**Option 2:** Navigate to `C:\Program Files\Zenstack\MyZenV2\updater.exe`, right-click, select **Properties**, then the **Details** tab. Note the version number.

### Stealth Mode

**Option 1:** Read the contents of:

```
C:\Program Files\zs\zs\version.txt
```

**Option 2:** Navigate to `C:\Program Files\zs\zs\updater.exe`, right-click, select **Properties**, then the **Details** tab. Note the version number.

> **Tip:** Check the **Download Apps** page in the We360.ai portal for the latest available version number to compare against.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.we360.ai/deployment-and-it-ops/deployment/agent-deployment-hub/manual-installation/windows-validation.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
