Skip to content

External Script Integration

Goal: Run your own scripts (PowerShell, batch, VBScript, or any executable) as monitoring checks, so NSClient++ can call them and return their result to your monitoring server.


Prerequisites

Enable the CheckExternalScripts module in nsclient.ini:

[/modules]
CheckExternalScripts = enabled
NRPEServer           = enabled   ; if using NRPE

How External Scripts Work

NSClient++ runs your script as a child process, captures its output, and passes the exit code and text back to the monitoring server.

Exit codes:

Exit code Status
0 OK
1 WARNING
2 CRITICAL
3 UNKNOWN

Output format:

Status message text|'metric_name'=value;warn;crit;min;max

The part after | is optional performance data. If you do not need graphs, output only the status text.


Adding a Script

Short format (most common)

[/settings/external scripts/scripts]
check_my_app  = scripts\check_my_app.bat
check_my_app2 = scripts\check_my_app.ps1

Each key becomes a new check command. The script path is relative to the NSClient++ installation directory.

Long format (for advanced options)

[/settings/external scripts/scripts/check_my_app]
command = scripts\check_my_app.bat

Use the long format when you need to set additional options such as running the script as a specific user.


Script Examples

Batch file (check_my_app.bat)

@echo off
rem Check if a file exists
if exist "C:\AppData\lockfile.lock" (
    echo WARNING: Lock file exists - application may be hung
    exit /b 1
)
echo OK: Application is running normally
exit /b 0

PowerShell script (check_memory_ps.ps1)

$mem = Get-CimInstance Win32_OperatingSystem
$usedPct = [math]::Round((($mem.TotalVisibleMemorySize - $mem.FreePhysicalMemory) / $mem.TotalVisibleMemorySize) * 100)

if ($usedPct -ge 90) {
    Write-Host "CRITICAL: Memory usage at $usedPct%"
    exit 2
} elseif ($usedPct -ge 80) {
    Write-Host "WARNING: Memory usage at $usedPct%"
    exit 1
} else {
    Write-Host "OK: Memory usage at $usedPct%|'memory_used_pct'=$usedPct%;80;90;0;100"
    exit 0
}

Register it:

[/settings/external scripts/scripts]
check_memory_ps = scripts\check_memory_ps.ps1

Script Wrappers

NSClient++ includes wrappers to run different script types. These are pre-configured in the [/settings/external scripts/wrappings] section:

[/settings/external scripts/wrappings]
bat = "scripts\%SCRIPT%" $ARGS"$
ps1 = powershell.exe -NoProfile -ExecutionPolicy Bypass -NonInteractive -File "scripts\%SCRIPT%" $ARGS"$
vbs = cscript.exe //T:30 //NoLogo "scripts\lib\wrapper.vbs" %SCRIPT% $ARGS"$

Using a wrapped script:

[/settings/external scripts/wrapped scripts]
check_updates = check_updates.vbs $ARG1$ $ARG2$

Note

Older versions of NSClient++ shipped a ps1 wrapping that piped through cmd.exe: cmd /c echo scripts\%SCRIPT% %ARGS% ; exit($lastexitcode) | powershell.exe -command -. The current default invokes powershell.exe -File directly so that $ARGn$ substitutions cannot pass through cmd.exe’s parser as statement-level metacharacters. Existing operator-supplied wrappings keep working unchanged; only the default templates were updated.


How Arguments Reach Your Script

When allow arguments = true, NSClient++ tokenises your command template once and substitutes $ARGn$ at the token level, then launches the script with the resulting argument vector directly:

  • On Unix, fork + execvp — no /bin/sh -c.
  • On Windows, CreateProcess with lpApplicationName set to argv[0] and a per-argument-quoted command line that round-trips through CommandLineToArgvW.

What this means in practice:

  • A $ARGn$ whose value contains spaces, ;, $, (, ), newlines or any other shell metacharacter reaches your script as a single argv element. It is no longer interpreted as command separation, redirection or sub-shell.
  • $ARGS$ / %ARGS% and the quoted-equivalent forms, when used as a standalone template token, splat the supplied arguments as separate argv elements. Embedded inside a larger token (e.g. prefix-$ARGS$-suffix) they collapse to a space-joined single token.

Quote command paths that contain spaces

Because the launcher now uses argv[0] to fix the executable, an unquoted path with spaces splits across argv:

# WRONG — argv[0] becomes "C:\Program" and the launch fails
[/settings/external scripts/scripts/check_app]
command = C:\Program Files\nscp\check.exe --foo

# RIGHT — the whole path is one token
[/settings/external scripts/scripts/check_app]
command = "C:\Program Files\nscp\check.exe" --foo

The bundled wrappings (scripts\%SCRIPT%) already follow this rule.


Common Gotchas

PowerShell exit 2 reported as WARNING (not CRITICAL)

If a PowerShell check script does exit 2 and the result reaches your monitoring server as WARNING instead of CRITICAL, the script’s exit code isn’t reaching NSClient++ — powershell.exe itself is exiting with 1.

This trips most people on first contact with PowerShell-based checks. It is a powershell.exe behaviour, not an NSClient++ bug.

# Broken — `exit 2` from the script becomes WARNING
[/settings/external scripts/scripts]
check_crit = powershell scripts\check_crit.ps1

When powershell.exe is invoked without -File, it runs the path in -Command mode and exits with 0 on success / 1 on any error, regardless of $LASTEXITCODE inside the script. The script’s exit 2 is silently dropped and NSClient++ sees 1 → WARNING.

Pick any of the three fixes:

Option 1 — use the .ps1 wrapping (recommended)

The bundled ps1 wrapping (in [/settings/external scripts/wrappings]) invokes powershell.exe -File so the script’s exit code is propagated verbatim. Move the entry to [wrapped scripts] and reference the file name:

[/settings/external scripts/wrapped scripts]
check_crit = check_crit.ps1

Option 2 — use -File

[/settings/external scripts/scripts]
check_crit = powershell -File scripts\check_crit.ps1

In -File mode powershell.exe returns the script’s exit code verbatim.

Option 3 — propagate $LASTEXITCODE explicitly

[/settings/external scripts/scripts]
check_crit = powershell -Command "scripts\check_crit.ps1; exit $LASTEXITCODE"

Same idea as the wrapping — the trailing exit $LASTEXITCODE forwards the inner script’s code.

Verify on the host before deploying:

powershell scripts\check_crit.ps1
echo %ERRORLEVEL%   :: prints 1 — the bug

powershell -File scripts\check_crit.ps1
echo %ERRORLEVEL%   :: prints 2 — correct

Using Arguments

[/settings/external scripts/scripts]
check_disk_c = scripts\check_disk.bat C: 20 10

The arguments C:, 20, and 10 are always passed to the script — they cannot be changed from the monitoring server.

Arguments from the monitoring server (allow arguments)

There are actually two independent allow arguments flags — one on the NRPE server, one on CheckExternalScripts — and the combination determines how exposed your scripts are. Pick the strategy that matches your threat model (if the agent also serves the REST API, the web role decides the same thing for those callers: restricted is the no-arguments one, monitoring and client pass them through):

Strategy NRPE allow arguments External-scripts allow arguments Trade-off
None — locked down (most secure) false false All thresholds hard-coded in nsclient.ini. Most secure; least flexible.
Built-ins only true false The monitoring server can drive thresholds for built-in commands (check_cpu, …) but cannot pass arbitrary arguments to your scripts. Good middle ground.
Both — fully remote-controlled true true Maximum flexibility; biggest blast radius. A compromised NRPE port can run your scripts with any arguments. Combine with strict allowed hosts and a firewall.

Built-ins-only configuration:

[/settings/NRPE/server]
allow arguments = true

[/settings/external scripts]
allow arguments = false

Both-allowed configuration (only with strict network restrictions):

[/settings/NRPE/server]
allow arguments = true

[/settings/external scripts]
allow arguments = true

[/settings/external scripts/scripts]
foo = scripts\foo.bat $ARG1$ $ARG2$

Arguments are accessed in scripts as $ARG1$, $ARG2$, etc.

Danger

Enabling argument pass-through is a security risk: any host that can reach the NRPE port can pass arbitrary arguments to whatever script you have defined. Combine with allowed hosts and a firewall.

NSClient++ now isolates each $ARGn$ substitution as a single argv element (no shell, no Windows command-line re-tokenisation), so an attacker cannot embed ;, $(), redirection, or extra flags via argument injection. They can still call any registered command with any string value, which is enough to drive a script into doing expensive work, hitting external systems, or exposing whatever the script chooses to expose. Treat your scripts as the security boundary and validate $ARGn$ inside the script itself.

Note

The allow nasty characters flag remains as defence in depth. With it set to false, NSClient++ rejects requests whose arguments contain |, `, &, >, <, ', ", \, [, ], {, }. It is no longer the only thing standing between the network and a shell interpreter — argv isolation is — but leaving it false continues to block the most obvious abuse patterns.

Locking the REST door too

These flags cover NRPE. A caller on the REST API holding queries.execute passes arguments regardless of what the NRPE server allows. Assign such clients the built-in restricted role instead — it grants queries.execute.noargs, so an argument-carrying request is refused — and the “locked down” row above holds on both transports. See Securing NSClient++.

Protocol payload limits

Each transport has its own hard-coded payload size limit. If your script output exceeds the limit, the result will be truncated or the protocol will reject it outright:

Protocol Limit
NRPE v2 1024 bytes
NRPE v3+ configurable, but practical limit is still small
NSCA 512 bytes

For long output, summarise in the message and put detail in the performance-data section, or split the check into multiple smaller checks.


Running a Script as a Different User

On Windows a script section can carry the account to run as:

[/settings/external scripts/scripts/check_as_admin]
command  = scripts\check_admin_resource.bat
user     = Administrator
password = s3cr3t_p@ssword

The user, domain and password keys are Windows-only. On Linux a script with any of them set is refused (the check returns UNKNOWN and the script does not run) rather than silently executed as the service account. Use sudo in the command instead and grant it in sudoers (NOPASSWD, with -n so the check can never block on a password prompt):

[/settings/external scripts/scripts/check_as_nobody]
command = sudo -n -u nobody /usr/lib/nagios/plugins/check_something

Programs That Should Keep Running

If a script starts a long-running process (e.g., a remediation action), it must not block NSClient++.
Set capture output = false to launch and immediately return:

[/settings/external scripts/scripts/fix_problem]
command        = scripts\fix_problem.bat
capture output = false

Danger

Do not use start or similar shell tricks to background a process inside a regular script. This causes handle inheritance issues that block NSClient++’s port until it is restarted. Use capture output = false instead.


Ignoring Performance Data

NSClient++ parses anything after | in your script’s output as Nagios performance data. If your script’s pipe character isn’t perfdata — or it emits non-conforming text that the parser then mangles — disable parsing.

Per-script:

[/settings/external scripts/scripts/check_foo]
command         = scripts\check_foo.bat
ignore perfdata = true

Globally for every external script:

[/settings/external scripts/scripts/default]
ignore perfdata = true

Testing Your Script

Use the NSClient++ test shell to verify your script works before connecting it to a monitoring server:

nscp test
check_my_app

You will see the exit code and output immediately.


Configuration Example

[/modules]
CheckExternalScripts = enabled
NRPEServer           = enabled

[/settings/external scripts]
allow arguments = false   ; keep this false unless you specifically need it

[/settings/external scripts/scripts]
check_my_app   = scripts\check_my_app.bat
check_my_svc   = scripts\check_service_status.ps1

[/settings/NRPE/server]
allowed hosts = 10.0.0.1
port          = 5666

On the monitoring server:

check_nrpe -H <agent-ip> -c check_my_app
check_nrpe -H <agent-ip> -c check_my_svc

Running a Script on a Schedule (Locally)

The Scheduler module is the same machinery used for passive monitoring, but its channel knob doesn’t have to point at a monitoring server. Two local-only channels are useful when you want NSClient++ to run a script periodically for reasons other than reporting to Nagios/Icinga:

Channel Effect Use case
noop Discard the result entirely Periodic remediation/maintenance scripts; fire-and-forget actions
file Append the result to a file via the SimpleFileWriter module Local audit trail; feeding output to log shippers or other tools

Fire and forget (channel = noop)

Run a remediation script every five minutes; do not push the result anywhere:

[/modules]
CheckExternalScripts = enabled
Scheduler            = enabled

[/settings/external scripts/scripts/cleanup_temp]
command = scripts\cleanup_temp.bat

[/settings/scheduler/schedules/cleanup_loop]
command  = cleanup_temp
interval = 5m
channel  = noop

Write results to a file (channel = file)

Append every run’s output to a local file — useful as an audit trail or to feed a log shipper:

[/modules]
CheckExternalScripts = enabled
Scheduler            = enabled
SimpleFileWriter     = enabled    ; provides the `file` channel

[/settings/external scripts/scripts/collect_inventory]
command = scripts\collect_inventory.ps1

[/settings/scheduler/schedules/inventory_hourly]
command  = collect_inventory
schedule = 18 * * * *             ; 18 minutes past every hour
channel  = file

By default SimpleFileWriter appends to output.txt next to nsclient.ini; see the module’s reference for redirecting elsewhere or rotating the file.

When to use which channel

Goal Channel See also
Push results to Nagios / Icinga / NRDP NSCA / NRDP / Icinga Passive Monitoring (NSCA/NRDP), Passive Monitoring (Icinga 2)
Run a script periodically; don’t care about output noop (this section)
Run a script periodically; capture output locally file (this section)

For the full scheduler syntax — interval vs cron-style schedule, per-job overrides, real-time channels — see Passive Monitoring → Configure the Scheduler.


Where to Find Scripts

You don’t have to write everything yourself. Community-maintained Nagios- compatible plugins live at:

Both work with NSClient++ as long as the script honours the standard exit-code convention (0 OK, 1 WARN, 2 CRIT, 3 UNKNOWN).


Next Steps