Event Hooks Implementation Guide

Step-by-step setup for Windows and macOS, plus fleet rollout via any RMM or MDM tool

  • Updated on

Note: This feature is supported on Windows devices with desktop app version 4.10.0+ and on macOS devices with desktop app version 4.2.0+. For an overview of how Event Hooks work, see Event Hooks for Post-Connection Scripts.

Overview

This guide walks through a working, end-to-end Event Hooks deployment that fires a script every time a user connects to a Service Tunnel. It covers:

  • The artifacts that need to land on each managed endpoint.
  • Step-by-step Windows setup using Task Scheduler.
  • Step-by-step macOS setup using launchd QueueDirectories.
  • Fleet rollout via any RMM or MDM tool, with N-able N-sight as the worked example.
  • A worked use case that replicates the legacy NetExtender Run domain login scripts behavior on Cloud Secure Edge.
  • Troubleshooting and a pilot/rollout plan.

Why script execution lives in the operating system, not in the desktop app: Allowing a remote access client to spawn arbitrary scripts opens an attack surface that endpoint detection software cannot easily monitor. Delegating execution to Windows Task Scheduler and macOS launchd keeps script execution inside a heavily managed, observable subsystem while preserving the same end-user outcome.

Prerequisites

  • CSE desktop app is installed on the endpoint and the user is enrolled.
  • The desktop app meets the minimum version listed in the note above.
  • A Service Tunnel is published to the resources the script needs to reach (file servers, domain controllers, internal hosts).
  • The endpoint protection product permits script execution from the path the Scheduled Task or launchd job calls. If endpoint protection is blocking the trigger directory or event channel, the hook will not fire.
  • For Windows domain logon scenarios: the endpoint is joined to on-premises Active Directory, and the user has a logon script defined in the AD Profile tab.

Windows implementation

The Windows path uses three artifacts placed on the endpoint:

File Purpose
C:\ProgramData\SCS\Invoke-EventHook.xml Scheduled Task definition. Listens for the CSE event and launches the hidden VBScript launcher.
C:\ProgramData\SCS\Invoke-EventHook.vbs One-line launcher that starts the PowerShell wrapper with no visible console window.
C:\ProgramData\SCS\Invoke-EventHook.ps1 Wrapper that performs the admin’s chosen action (drive mapping, gpupdate, custom logic).
  1. Open Event Viewer with administrative rights on a pilot endpoint.
  2. Navigate to Windows LogsApplication.
  3. Disconnect and reconnect the CSE tunnel.
  4. Confirm an event with Source = CSEService and Event ID = 9001 appears at the moment the tunnel reaches the connected state.

Save the following XML as Invoke-EventHook.xml. The task runs in the interactive user’s context, fires on Event ID 9001, and calls wscript.exe against the VBScript launcher defined in Step 3.

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Triggers>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>
        &lt;QueryList&gt;
          &lt;Query Id="0" Path="Application"&gt;
            &lt;Select Path="Application"&gt;
              *[System[Provider[@Name='CSEService'] and EventID=9001]]
            &lt;/Select&gt;
          &lt;/Query&gt;
        &lt;/QueryList&gt;
      </Subscription>
    </EventTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <GroupId>S-1-5-32-545</GroupId>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>true</StartWhenAvailable>
    <Enabled>true</Enabled>
    <Hidden>true</Hidden>
    <ExecutionTimeLimit>PT10M</ExecutionTimeLimit>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>wscript.exe</Command>
      <Arguments>"C:\ProgramData\SCS\Invoke-EventHook.vbs"</Arguments>
    </Exec>
  </Actions>
</Task>

The S-1-5-32-545 principal is the built-in Users group, which ensures the task runs as the signed-in user. User context is required for outcomes such as drive mappings. The <Hidden>true</Hidden> setting suppresses the task entry from the default Task Scheduler view.

Save the following as Invoke-EventHook.vbs. Because wscript.exe has no console host, and the second argument to Run is 0 (SW_HIDE), neither this file nor the PowerShell process it launches draws anything on the user’s screen.

' Invoke-EventHook.vbs
' Silent launcher for the PowerShell wrapper.
Dim shell, q
Set shell = CreateObject("Wscript.Shell")
q = Chr(34)
shell.Run "powershell.exe -ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File " & q & "C:\ProgramData\SCS\Invoke-EventHook.ps1" & q, 0, False
Set shell = Nothing

Save the following as Invoke-EventHook.ps1. This example replicates the legacy NetExtender Run domain login scripts behavior by reading the scriptPath attribute of the logged-on user from Active Directory at execution time, running it, and refreshing Group Policy so any GPO drive maps apply. Replace this block with any other action you want to fire on connect.

# Invoke-EventHook.ps1
# Example: run the domain logon script defined for the current user in AD,
# then refresh Group Policy.
$ErrorActionPreference = 'SilentlyContinue'
$logDir = 'C:\ProgramData\SCS\Logs'
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
Start-Transcript -Path (Join-Path $logDir ('eventhook-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.log'))

# Wait briefly for the CSE tunnel to settle and DNS to resolve a domain controller.
Start-Sleep -Seconds 2
$success = $false
$scriptName = $null

do {
    try {
        $searcher = New-Object DirectoryServices.DirectorySearcher
        $searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$env:USERNAME))"
        $null = $searcher.PropertiesToLoad.Add('scriptPath')
        $result = $searcher.FindOne()
        if ($result -ne $null) {
            $scriptName = $result.Properties['scriptpath'] | Select-Object -First 1
            $success = $true
        }
    } catch {
        Write-Output "AD lookup failed: $_"
        Start-Sleep -Seconds 3
    }
} while (-not $success)

$logonServer = $env:LOGONSERVER
if (-not $logonServer) { $logonServer = "\\$env:USERDNSDOMAIN" }

if ($scriptName) {
    $fullPath = Join-Path ($logonServer + '\NETLOGON') $scriptName
    Write-Output "Executing $fullPath"
    switch -Wildcard ($scriptName) {
        '*.vbs' { Start-Process 'wscript.exe'    -ArgumentList "//B //Nologo `"$fullPath`"" -WindowStyle Hidden -Wait }
        '*.bat' { Start-Process 'cmd.exe'        -ArgumentList "/c `"$fullPath`""           -WindowStyle Hidden -Wait }
        '*.cmd' { Start-Process 'cmd.exe'        -ArgumentList "/c `"$fullPath`""           -WindowStyle Hidden -Wait }
        '*.ps1' { Start-Process 'powershell.exe' -ArgumentList "-ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File `"$fullPath`"" -WindowStyle Hidden -Wait }
        default { Write-Output "Unsupported logon script extension: $scriptName" }
    }
} else {
    Write-Output "No scriptPath set on AD user $env:USERNAME"
}

# Refresh Group Policy so GPO drive maps apply once the tunnel is up.
Start-Process 'gpupdate.exe' -ArgumentList '/target:user /force' -WindowStyle Hidden -Wait
Stop-Transcript

Key properties of this wrapper:

  • Dynamic. The wrapper reads the current scriptPath from Active Directory at execution time. Changes made in the user’s AD Profile tab take effect on the next connect with no redeployment.
  • GPO-aware. The gpupdate call applies standard and conditional Group Policy drive maps.
  • Silent. Every child process is launched with -WindowStyle Hidden. The user sees no console flash.
  • Logged. Transcripts land in C:\ProgramData\SCS\Logs for validation on pilot devices.

On the pilot device, register the task once manually:

schtasks /Create /TN "SonicWall\CSE Event Hook" /XML "C:\ProgramData\SCS\Invoke-EventHook.xml" /F

Disconnect and reconnect the tunnel. Confirm the transcript log in C:\ProgramData\SCS\Logs\ and the expected drives appear under This PC.

macOS implementation

The macOS path uses two artifacts placed on the endpoint:

File Purpose
~/Library/LaunchAgents/com.sonicwall.cse.eventhook.plist launchd job. Watches the CSE connect queue directory and runs the wrapper script.
~/Library/Application Support/sonicwallcse/scripts/event-hook.sh Wrapper script that performs the admin’s chosen action.

Save the following to ~/Library/Application Support/sonicwallcse/scripts/event-hook.sh. Replace the action block with whatever you want to run on connect.

#!/bin/bash
# event-hook.sh — fires when CSE drops a trigger file in the connect queue.

set -e
log_dir="$HOME/Library/Application Support/sonicwallcse/Logs"
mkdir -p "$log_dir"
log_file="$log_dir/eventhook-$(date +%Y%m%d-%H%M%S).log"
exec >> "$log_file" 2>&1

echo "CSE connect event received at $(date)"

# Drain the queue so launchd does not re-fire on the same file.
queue="$HOME/Library/Application Support/sonicwallcse/Queues/connect"
find "$queue" -type f -name 'trigger_*' -delete

# --- Replace this block with your action -----------------------------
# Example: mount an SMB share once the tunnel is up.
osascript -e 'mount volume "smb://files.internal.example.com/team"' || true
# ---------------------------------------------------------------------

Make the script executable:

chmod +x ~/Library/Application\ Support/sonicwallcse/scripts/event-hook.sh

Save the following to ~/Library/LaunchAgents/com.sonicwall.cse.eventhook.plist.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.sonicwall.cse.eventhook</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>-c</string>
        <string>"$HOME/Library/Application Support/sonicwallcse/scripts/event-hook.sh"</string>
    </array>
    <key>QueueDirectories</key>
    <array>
        <string>~/Library/Application Support/sonicwallcse/Queues/connect</string>
    </array>
    <key>RunAtLoad</key>
    <false/>
</dict>
</plist>

To hook the disconnect path instead, change the QueueDirectories entry to ~/Library/Application Support/sonicwallcse/Queues/disconnect.

launchctl load ~/Library/LaunchAgents/com.sonicwall.cse.eventhook.plist

Disconnect and reconnect the CSE tunnel. Confirm the transcript log in ~/Library/Application Support/sonicwallcse/Logs/ and the expected outcome (mounted share, refreshed credential, etc.).

Deploying to a fleet via RMM or MDM

Any device management tool that can place files on a managed endpoint and register a Scheduled Task or load a launchd plist can deploy Event Hooks at scale. The general pattern is the same regardless of which tool you use.

  1. Package the three Windows artifacts (Invoke-EventHook.xml, Invoke-EventHook.vbs, Invoke-EventHook.ps1) and the two macOS artifacts (com.sonicwall.cse.eventhook.plist, event-hook.sh) into your tool’s deployment unit (script object, Win32 app, configuration profile, etc.).
  2. Deploy a bootstrap script that copies the artifacts into place and registers the Scheduled Task or loads the launchd plist:
    • Windows: schtasks /Create /TN "SonicWall\CSE Event Hook" /XML "C:\ProgramData\SCS\Invoke-EventHook.xml" /F
    • macOS: launchctl load ~/Library/LaunchAgents/com.sonicwall.cse.eventhook.plist
  3. Run the bootstrap on a recurring schedule (for example, daily at a quiet hour) so any reverted endpoint self-heals. The /F flag in schtasks and idempotent file copy on macOS make the bootstrap safe to re-run.
  4. Apply the policy to device groups by site, customer, or role.

Note: All major RMM and MDM platforms (N-able N-sight, Microsoft Intune, ConnectWise Automate, NinjaOne, Kaseya VSA, Jamf Pro) support the file-drop plus task-registration model described above. The artifacts in this guide are tool-agnostic.

The following sequence uses N-sight’s Automation Policy framework. The same approach applies to other RMM platforms with naming differences only.

1. Upload the wrapper script

  1. In N-sight, open SettingsGeneral SettingsScript Manager.
  2. Choose Upload and add Invoke-EventHook.ps1. Set the script type to PowerShell.
  3. Record the script ID for reference.

2. Create the deployment script

Save the following as Deploy-CSEEventHook.ps1 and upload it as a second PowerShell script object. Mark it to run in the SYSTEM context.

# Deploy-CSEEventHook.ps1
$target = 'C:\ProgramData\SCS'
New-Item -ItemType Directory -Force -Path $target | Out-Null

Copy-Item -Path '.\Invoke-EventHook.ps1' -Destination "$target\Invoke-EventHook.ps1" -Force
Copy-Item -Path '.\Invoke-EventHook.vbs' -Destination "$target\Invoke-EventHook.vbs" -Force
Copy-Item -Path '.\Invoke-EventHook.xml' -Destination "$target\Invoke-EventHook.xml" -Force

schtasks /Create /TN "SonicWall\CSE Event Hook" /XML "$target\Invoke-EventHook.xml" /F

Bundle Invoke-EventHook.ps1, Invoke-EventHook.vbs, and Invoke-EventHook.xml as additional files inside the N-sight script package so the agent can find all three at run time.

3. Build the Automation Policy

  1. Open SettingsAutomationAutomation Policies.
  2. Create a new policy named CSE Event Hook Bootstrap.
  3. Add Deploy-CSEEventHook.ps1 as the action.
  4. Set the schedule to Daily at a quiet hour so reverted endpoints self-heal within twenty-four hours.
  5. Apply the policy to the device groups that map to the targeted customer sites.

Worked use case: NetExtender domain logon script parity

Admins migrating from SonicWall NetExtender often rely on the Run domain login scripts option to map drives and apply Group Policy after the VPN authenticates. Cloud Secure Edge does not include a client-side script execution toggle, but the Windows wrapper in this guide produces equivalent end-user behavior with no per-user RMM customization.

The wrapper resolves the user’s scriptPath attribute against Active Directory at execution time, so:

  • Changes to a user’s logon script in the AD Profile tab take effect on the next connect with no RMM redeployment.
  • Different users can have entirely different logon scripts (for example, finance.vbs versus support.vbs); the wrapper picks up whatever is assigned to the signed-in user.
  • Conditional and location-specific Group Policy drive maps continue to apply because the wrapper triggers gpupdate.

This is the supported migration path from NetExtender’s Run domain login scripts checkbox.

Troubleshooting

Symptom Likely cause Resolution
Event ID 9001 does not appear in Event Viewer. Endpoint protection software is blocking the CSE event writer. Add the CSE Connect agent install path to the antivirus exclusion list, then reconnect the tunnel.
Task runs but no drives appear. Task is executing as SYSTEM rather than the interactive user. Confirm the Principal in the Scheduled Task XML is S-1-5-32-545 (Users) and Context is Author, not Highest.
Drives appear but GPO maps are missing. gpupdate ran before a domain controller was reachable. Increase the Start-Sleep value in the wrapper from 2 seconds to 5–15 seconds depending on tunnel ramp-up time.
Script runs twice on reconnect. CSE wrote more than one tunnel-up event during the reconnect. The MultipleInstancesPolicy value of IgnoreNew suppresses overlapping runs. If they still occur, add a one-minute file-timestamp guard.
PowerShell execution policy blocks the wrapper. Group Policy restricts script execution. The launcher already passes -ExecutionPolicy Bypass. Confirm no Constrained Language Mode policy is enforced on the endpoint.
A console window briefly flashes on the user’s screen. Task is calling powershell.exe directly instead of the wscript.exe launcher. Confirm the Scheduled Task Action is wscript.exe with Invoke-EventHook.vbs, not powershell.exe with the .ps1 file.
macOS launchd job does not fire. The plist was not loaded, or the QueueDirectories path was quoted incorrectly. Run launchctl list | grep cse.eventhook. If absent, launchctl load the plist. Confirm the path uses ~/Library/Application Support/....
macOS job fires once and never again. The script did not drain the queue directory, so launchd does not observe new files. Confirm the wrapper deletes processed files from the queue directory (see Step 1 of the macOS section).

Pilot and rollout plan

Phase Scope Exit criteria
Lab validation One technician workstation joined to your test domain. Tunnel-up event fires, Scheduled Task runs, transcript shows the expected action completed.
Customer pilot Five endpoints, including at least one with conditional GPO maps. All pilot users report the expected outcome within thirty seconds of connect for three consecutive days.
Site rollout Remaining endpoints at the pilot site. Help desk volume related to the hooked workflow does not increase over the prior week’s baseline.
Fleet rollout Remaining sites, grouped by deployment ring. Standing automation policy in your RMM covers every newly enrolled CSE device.

Forward-looking notes

VBScript deprecation

Microsoft has announced that VBScript will move from default-disabled to fully removed in future Windows servicing updates. The wrapper script in the Windows section handles .vbs, .bat, .cmd, and .ps1 logon scripts, so the same deployment carries forward as you migrate users off VBScript. To migrate a script, change the scriptPath attribute on the corresponding user in Active Directory. No change is required in your RMM or in the Scheduled Task.

Support

For support questions outside the scope of this guide, open a case at https://helpdesk.sonicwall.com.