#9114·osquery

scheduled_tasks.hidden returns the task's Enabled state instead of Hidden (Windows)

Author: Jun9-bsCreated Sep 22, 2026Updated Sep 25, 2026
Labelsbugvirtual tablestriageWindows

Bug report

What operating system and version are you using?

 version = 10.0.26200
   build = 26200
platform = windows

What version of osquery are you using?

version = 5.23.1

The code path is unchanged on master (5a4516d).

What steps did you take to reproduce the issue?

Query the table and compare it against the Task Scheduler API.

sql
SELECT name, enabled, hidden FROM scheduled_tasks;
powershell
Get-ScheduledTask | Select-Object TaskName,
    @{n='Enabled'; e={$_.Settings.Enabled}},
    @{n='Hidden';  e={$_.Settings.Hidden}}

For a controlled check, register two tasks with opposite Hidden/Enabled combinations:

powershell
$act = New-ScheduledTaskAction -Execute cmd.exe

# visible + enabled
Register-ScheduledTask -TaskName osq_visible_enabled -Action $act `
    -Settings (New-ScheduledTaskSettingsSet)

# hidden + disabled
Register-ScheduledTask -TaskName osq_hidden_disabled -Action $act `
    -Settings (New-ScheduledTaskSettingsSet -Hidden -Disable)
sql
SELECT name, enabled, hidden FROM scheduled_tasks
WHERE name LIKE 'osq\_%' ESCAPE '\';

What did you expect to see?

hidden should reflect ITaskSettings::Hidden, i.e. the task registered with -Hidden should be the one reported as hidden:

+---------------------+---------+--------+
| name                | enabled | hidden |
+---------------------+---------+--------+
| osq_hidden_disabled | 0       | 1      |
| osq_visible_enabled | 1       | 0      |
+---------------------+---------+--------+

What did you see instead?

hidden tracks enabled, so the values come out inverted relative to the Hidden setting that was actually applied:

+---------------------+---------+--------+
| name                | enabled | hidden |
+---------------------+---------+--------+
| osq_hidden_disabled | 0       | 0      |
| osq_visible_enabled | 1       | 1      |
+---------------------+---------+--------+

The same holds over a full enumeration. Running osqueryi as an administrator returned 292 rows, and hidden == enabled on 292/292 of them. Matching those rows by name against Get-ScheduledTask (207 could be paired):

hidden vs Settings.Enabled    207 / 207 agree
hidden vs Settings.Hidden      48 / 207 agree

The 48 are coincidences — tasks that happen to be both enabled and hidden, or both disabled and visible.

Root cause

The hidden column is populated from get_Enabled():

https://github.com/osquery/osquery/blob/5a4516d56c27fb73168272faf66d7373e5f35018/osquery/tables/system/windows/scheduled_tasks.cpp#L113-L115

cpp
VARIANT_BOOL hidden = false;
pRegisteredTask->get_Enabled(&hidden);
r["hidden"] = hidden ? INTEGER(1) : INTEGER(0);

The same getter fills enabled a few lines above (L97-L99), so the two columns can never differ. get_Hidden does not appear anywhere in the file.

IRegisteredTask has no Hidden property — it is on ITaskSettings, reached via IRegisteredTask::get_Definition() -> ITaskDefinition::get_Settings() -> ITaskSettings::get_Hidden(). The function already calls get_Definition() at L143 to enumerate actions, so the settings object is available at that point.

The line has been present since the table was introduced in #3153 (April 2017, 6a90db47be); subsequent commits to this file are licensing, refactor, leak and type fixes that do not touch this assignment.

Impact

Any query filtering on hidden silently selects every enabled task instead. Hidden scheduled tasks — commonly used for persistence and defense evasion — cannot be identified from this table at all, and the column gives a false sense that they can.