runas and UAC token filtering: why your admin window is not elevated

runas starts a program under a different account than the one you are signed in with. It is the built-in answer to the rule everyone agrees with and nobody enjoys: sign in as yourself, and borrow administrative rights only for the command that needs them.

The trouble is what most people think it does. Typing runas /user:SRV-PROD-01\opsadmin cmd opens a new black window titled with the admin account name, and that window looks exactly like an elevated prompt. It is not one. On a default machine it carries a filtered standard user token, and the first command in it that needs administrative rights fails with an error that reads like a permissions problem rather than an elevation problem.

This article covers what runas actually hands you, the one account on every machine that is the documented exception to the rule, how to check the token you got instead of guessing, and where the same filtering shows up again over the network. The Windows Commands reference will not help you with any of it, because runas no longer has one.

Applies to: Windows 10 and Windows 11, and supported versions of Windows Server


Quick answer

Start a program as another account, then immediately check which token you were given. The second command is the part people skip, and it is the one that saves the afternoon.

rem Start a command prompt as another local account. You will be prompted for the password.
rem runas prints nothing on success: the new window IS the output.
runas /user:SRV-PROD-01\opsadmin cmd

rem Now, INSIDE that new window, ask two questions.
rem 1. Who am I? 2. Is this token elevated?
whoami
whoami /groups | findstr /c:"S-1-16-12288"

If the second command prints a line, the window is running at High integrity and is elevated. If it prints nothing and returns to the prompt, you have a filtered token: the account is an administrator, but this process is not using its administrative rights.

Warning: An empty result from that findstr is the normal case, not a failure. On a default machine runas with an ordinary administrator account produces a non elevated window every time.

What runas actually does

runas creates a new logon session for the account you name and starts your program inside it. The work is done by a Windows service called Secondary Logon, whose service name is seclogon and whose documented job is “Enables starting processes under alternate credentials”. Its startup type is Manual, which matters later.

Creating a logon session is the whole story, and it is also the whole problem. User Account Control applies its token split at logon. Microsoft states it plainly: “When an administrator logs on, two separate access tokens are created for the user: a standard user access token and an administrator access token.” The standard token is the one processes get by default. The full one is handed out by a different component entirely.

Note: The component that starts a process with the full administrative token is the Application Information service, which Microsoft describes as creating “a new process for the application with an administrative user’s full access token when elevation is required”. runas goes through Secondary Logon, not through Application Information. That is the mechanical reason it cannot elevate.

These are the parameters, taken from Microsoft’s archived reference page, which is the only reference page this command has:

ParameterWhat it doesWorth knowing
/user:<UserAccountName>The account to run as, as DOMAIN\user or user@domain.Required. The program and its path go in quotes after it.
/profileLoads the target user’s profile. This is the default.Cannot be combined with /netonly.
/noprofileDoes not load the profile. Starts faster.Some applications malfunction without their profile.
/envUses the current environment instead of the target user’s.Useful when you want your own %TEMP% and PATH.
/netonlyThe credentials are for remote access only.The process still runs as you locally. Covered in example 3.
/savecredReuses credentials previously saved by this user.Writes an entry into Credential Manager. Cannot be combined with /smartcard.
/smartcardTakes the credentials from a smart card.Cannot be combined with /savecred.
/showtrustlevelsLists the trust levels available for /trustlevel.Restricts a process further. It never raises privileges.
/trustlevel:<Level>Runs the program at one of those trust levels.Lowers authorization only.
Common mistake: There is no runas switch that elevates. /trustlevel looks like the one, and it goes the other way: it restricts the process. If you need elevation, you need a different mechanism, not a different flag.

Before the first example

Four things need to be true before any of the examples below behave the way this article says they will. Each one is a single command.

1. A second account that is an ordinary member of Administrators. Not the built-in Administrator account, which behaves differently and is the subject of its own section further down. Confirm the membership:

rem Read the local Administrators group. The account you will run as must be in here,
rem and it must NOT be the built-in Administrator account.
net localgroup Administrators

2. The Secondary Logon service must be running or startable. Its startup type is Manual, so a stopped service is normal, but a disabled one breaks runas completely:

rem sc qc prints the service configuration, including its start type.
rem Microsoft documents this service's startup type as Manual, so DEMAND_START is expected.
rem DISABLED is the one that breaks runas, and it breaks Run as administrator with it.
sc qc seclogon

3. A folder for the test script used in the parsing section, so nothing is written anywhere surprising:

rem Everything this article creates lives here and nowhere else.
md C:\bat 2>nul

4. A way to read the token you were given. whoami /groups is the instrument this whole article leans on. Run it once in your normal prompt so you know what the baseline looks like:

rem Print the integrity-level row for the session you are in right now.
whoami /groups | findstr /c:"Mandatory Label"

The row it prints carries an S-1-16- SID in its last column. Microsoft documents the string names for those SIDs: S-1-16-8192 is Mandatory Label\Medium Mandatory Level and S-1-16-12288 is Mandatory Label\High Mandatory Level. Medium is an ordinary process. High is an elevated one. If you want the full tour of what else is in that token, the whoami /all guide covers every section of it.


Why the new window is not elevated

Two facts sit underneath every confusing thing runas does, and both come straight from Microsoft’s own documentation.

The administrators SID is still in the filtered token. The UAC overview says the standard user token has “the administrative Windows privileges and SIDs removed”. The Win32 access token reference describes something more precise: a SID carrying the SE_GROUP_USE_FOR_DENY_ONLY attribute, where “the system checks for access-denied ACEs that apply to the SID, but it ignores access-allowed ACEs for the SID”. The same page adds that such a SID “cannot be reenabled”.

Note: Those two pages describe the same result at different levels of detail, and the difference is the one that bites you. Practically the group is present but useless for granting access, which is why whoami /groups in a non elevated window shows BUILTIN\Administrators with Group used for deny only rather than omitting it.
Common mistake: This is why a script that tests elevation by looking for the word Administrators in the group list is wrong on every machine it runs on. The group is there in both tokens. Membership does not tell you which token you are holding.

The integrity level is the field that actually differs. Microsoft’s integrity mechanism reference states that “an access token with the local Administrators group SID present is assigned the integrity level of high, and an access token for a standard user account is assigned the integrity level of medium”. In a filtered token the Administrators SID is deny only, so the token is Medium. That single field is the reliable answer, and it is why every check in this article reads the integrity level rather than the group list.

What you are looking atFiltered tokenFull token
BUILTIN\Administrators in the group listPresent, Group used for deny onlyPresent and enabled
Integrity level rowMandatory Label\Medium Mandatory LevelMandatory Label\High Mandatory Level
Integrity level SIDS-1-16-8192S-1-16-12288
Useful as an elevation testNo, the group is in bothYes, the SID is in one

The one account that is the exception

There is a reason so many experienced administrators are certain that runas elevates: they tested it with the built-in Administrator account, and for that account it does.

Microsoft documents a UAC setting called Admin Approval Mode for the built-in Administrator account. Its description of the disabled state is unambiguous: “Disabled (default): The built-in Administrator account runs all applications with full administrative privilege.” The registry value behind it is FilterAdministratorToken, under HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System, and the documented default is 0, meaning disabled.

So the same command against two accounts that are both in the same group gives two different answers:

rem Account A: an ordinary member of Administrators. Filtered token, Medium integrity.
runas /user:SRV-PROD-01\opsadmin cmd

rem Account B: the BUILT-IN Administrator. Not in Admin Approval Mode by default,
rem so this one really does come up with a full token at High integrity.
runas /user:SRV-PROD-01\Administrator cmd
Warning: Do not build a process around account B. The built-in Administrator account is disabled by default on client Windows, and hardened environments enable FilterAdministratorToken precisely to close this gap. A procedure that only works because one account is exempt from UAC stops working the day someone tightens the policy.

Read the value before you assume either behaviour. This is a read, and it changes nothing:

rem 0x0 means the built-in Administrator is NOT in Admin Approval Mode, the documented default.
rem 0x1 means it is, and then even that account gets a filtered token.
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v FilterAdministratorToken
Note: If the value is missing entirely, the documented default applies. Absent and 0 mean the same thing here.

Practical examples

Every example below uses the same machine SRV-PROD-01, the same ordinary admin account opsadmin, and the same folder C:\bat. The last one undoes what the fourth one creates.

1. Run a command prompt as another account and find out who you really are

The problem: You are signed in as yourself, you need to run a handful of commands as the operations admin account, and you want to be sure the window you get is actually running as that account.

The solution: Start cmd through runas, then ask the new window directly. runas itself prints nothing useful, so the confirmation has to come from inside.

rem A bare program name needs no quotes. Add arguments and the whole thing does: see Hidden gems.
rem You are prompted for the password interactively. runas cannot take it on the command line.
runas /user:SRV-PROD-01\opsadmin cmd

rem In the NEW window: confirm the identity before you trust the window.
whoami

whoami prints the domain or machine name and the user name of the account the process is running as, in the form Microsoft’s reference shows it, with the authority first and a backslash between:

SRV-PROD-01\opsadmin
Result: The identity switch worked. That is the only thing this confirms. It says nothing at all about elevation, which is the next example.

2. Prove the window is not elevated before a command fails

The problem: The window says opsadmin, opsadmin is an administrator, and yet a command that needs administrative rights is about to fail. You would rather know now than halfway through a change.

The solution: Read the integrity level. One line, and it answers the question the window title cannot.

rem S-1-16-12288 is the High integrity level and appears ONLY in an elevated token.
rem findstr sets ERRORLEVEL 0 when it matches and 1 when it does not, so this is scriptable.
whoami /groups | findstr /c:"S-1-16-12288"

rem Compare against the group row, which is present in BOTH tokens and proves nothing.
whoami /groups | findstr /c:"BUILTIN\Administrators"

On a default machine the first command prints nothing and the second prints a row. That combination is the signature of a filtered token: you are in the group, and the group is not helping you.

Warning: The failure, when it arrives, will not mention elevation. Microsoft documents the mechanism one layer down: CreateProcess rejects a call that needs elevation with ERROR_ELEVATION_REQUIRED, and it is the shell that turns that into a UAC prompt. A program started from a runas window has no shell in front of it to do that.

3. Use /netonly when the rights you need are on the other machine

The problem: You need to reach a share or a management console on another server with a different account, but the tool you are running has to keep working with your own local profile, your own drives and your own settings.

The solution: /netonly. Microsoft’s description is exact: the credentials “are for remote access only”. Locally the process keeps running as you.

rem The credentials are used ONLY when this process talks to the network.
rem Locally it is still running as YOU, with your profile and your token.
runas /netonly /user:CONTOSO\svc-deploy cmd

rem In the new window: this prints YOUR name, not svc-deploy. That is correct, not a failure.
whoami
Common mistake: This is the single most reported runas “bug” that is not a bug. whoami in a /netonly window prints the account you started from, so people conclude the command was ignored and type it again. The credentials are there. They are just reserved for the next network hop.

Two further documented details: /netonly cannot be combined with /profile, and the reference notes that an attempt to start a program from a network location under runas can fail outright, because the credentials used to reach the share are not the credentials used to start the program.

Map a drive from inside that window to see the borrowed credentials in action, and the net use guide covers what happens when two sets of credentials reach the same server at once.

4. Save the credential once with /savecred, and see what it left behind

The problem: You are running the same tool as the same admin account twenty times this morning and typing the password every time.

The solution: /savecred stores the credential so it is not requested again. It is genuinely convenient, and it is worth looking at what it wrote.

rem First run: you are prompted once, and the credential is stored.
runas /savecred /user:SRV-PROD-01\opsadmin cmd

rem Now list what is in Credential Manager for this user.
cmdkey /list
Warning: The stored credential belongs to your user profile, not to the command you typed it for. Any later runas /savecred for the same target account reuses it without prompting, whatever program is being started. Treat /savecred on a shared or privileged workstation as a decision, not a convenience.

5. Remove the saved credential again

The problem: The morning’s work is finished and you would rather not leave an administrative credential cached on the machine.

The solution: cmdkey /delete removes exactly the entry you created and nothing else. Read the list first so you delete the right target name.

rem Read the target name from the Target: line of cmdkey /list, then delete that one entry.
cmdkey /list

rem Delete only the entry created above. This removes nothing else.
cmdkey /delete:SRV-PROD-01

rem Confirm it is gone.
cmdkey /list
Result: The target no longer appears in cmdkey /list, and the next runas /savecred for that account prompts for the password again.

Reading the token without getting a wrong answer

Checking the integrity level by eye is fine once. Putting the check at the top of a script is better, and that is where CMD’s tokenizer turns a one line job into a trap.

The row you are parsing carries the label string in the first column and the SID in the third. Microsoft documents both pieces: the label string Mandatory Label\High Mandatory Level and the SID S-1-16-12288. Put together, the row has this shape, with runs of spaces between the columns:

Mandatory Label\High Mandatory Level                      Label            S-1-16-12288

Now the trap. for /f splits on spaces by default, and it collapses every run of spaces into one delimiter, so the column layout is irrelevant and the word boundaries are everything. Reaching for tokens=1 or tokens=3 feels natural and both return a useless word:

What you ask forWhat you get backWhy
tokens=1MandatoryThe label string itself contains spaces, so column one is three tokens.
tokens=2Label\HighThe level, glued to the label prefix by the backslash.
tokens=3MandatoryThe second half of the label string. Looks plausible, means nothing.
tokens=4LevelStill inside column one.
tokens=5LabelColumn two, the SID type.
tokens=6S-1-16-12288Column three. This is the one you want.
Note: Indentation does not change any of this. for /f skips leading delimiters before it starts counting, so the same row indented by four spaces still gives S-1-16-12288 at tokens=6. That is one thing you do not have to defend against.

There is a simpler route, though, and it is the one to reach for first. You do not need the token number at all if you match the SID directly, because only one row in the whole output contains it:

rem Matching the SID needs no tokenizing and no column counting.
rem ERRORLEVEL is 0 if the string was found and 1 if it was not.
whoami /groups | findstr /c:"S-1-16-12288"
Common mistake: Filtering on the phrase Mandatory Level instead of the SID is not an elevation test. Both the High row and the Medium row contain that phrase, so the filter matches in either case and you still have to read the value. Match the SID, or read tokens=6. Do not match the words.

Put together, this is a check you can paste at the top of any script that must not run half way. Note the division of labour in it: findstr only picks the row out, and the decision is made afterwards by comparing the SID that tokens=6 extracted. Filtering on the phrase is safe when it selects, and wrong when it decides.

@echo off
rem Save this as C:\bat\check-elevation.cmd
rem In a batch file the loop variable takes two percent signs, as below.
setlocal
set "IL="
for /f "tokens=6" %%L in ('whoami /groups ^| findstr /c:"Mandatory Label"') do set "IL=%%L"

if "%IL%"=="S-1-16-12288" (
    echo Elevated. Integrity level SID is %IL%.
    exit /b 0
)
echo NOT elevated. Integrity level SID is %IL%.
echo Start this script from an elevated prompt, not from a runas window.
exit /b 1

One percent sign or two: which form goes where

That loop is written for a batch file. Typed straight into a command prompt it fails immediately, and the only thing that changes between the two forms is the percent sign.

Where you are typing itLoop variableThe same line in full
A CMD promptone percent signfor /f "tokens=6" %L in ('whoami /groups ^| findstr /c:"Mandatory Label"') do @echo %L
A .bat or .cmd filetwo percent signsfor /f "tokens=6" %%L in ('whoami /groups ^| findstr /c:"Mandatory Label"') do @echo %%L

Microsoft states the rule in one sentence: use a single percent sign to carry out the for command at the command prompt, and double percent signs to carry it out within a batch file. Paste the batch form into a prompt and CMD stops before it runs anything:

%%L was unexpected at this time.
Common mistake: The percent sign is the only character that differs between those two lines, which is exactly why the mistake survives being copied out of a blog post or a forum answer. The quiet direction is worse than the loud one: a single percent sign inside a batch file is not a syntax error, the variable is ignored, an error message is displayed, and the script carries on. A skipped loop in a long script is noticed only when its output is missing.

The same filtering over the network

Once you have seen token filtering locally you start recognising it remotely, because Windows applies the same idea to network logons and the rule there has an asymmetry that catches people out.

Microsoft’s article on UAC and remote restrictions splits it by account type. For a local account in the target machine’s Administrators group, connecting remotely means the user “won’t connect as a full administrator. The user has no elevation potential on the remote computer.” For a domain account in the Administrators group, “the domain user will run with a full administrator access token on the remote computer, and UAC won’t be in effect.”

Warning: That is the reason a remote administration script passes against a domain joined server and fails against a workgroup box with the same local administrator account and the same password. Nothing about the account changed. The account type did.

The behaviour for local accounts is controlled by a registry value named LocalAccountTokenFilterPolicy, in the same key as the setting in the previous section. Microsoft documents both of its values:

ValueDocumented meaning
0“This value builds a filtered token. It’s the default value. The administrator credentials are removed.”
1“This value builds an elevated token.”
rem Read the remote-restriction value. Absent means the default, which is 0.
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v LocalAccountTokenFilterPolicy
Common mistake: Setting this to 1 is a documented change, and it is also the change that turns one reused local administrator password into lateral movement across every machine that shares it. Microsoft’s own page frames the default as protection against loopback attacks and against local malicious software running remotely with administrative rights. Prefer a domain account for remote administration and leave the value alone.

Hidden gems

Group Policy is not processed for the runas account, by design. The reference states it outright, and explains that the profile runas loads “may contain registry keys and values from previous interactive logons when Group Policy was processed for that user”. So a policy driven setting may appear to apply in a runas window purely because it was cached the last time that account signed in properly. Test policy by signing in, not by borrowing an account.

The documented syntax puts the program and its path inside one set of quotes, together. The reference writes the parameter as /user: followed by "<ProgramName> <PathToProgramFile>", and every Microsoft example that passes an argument is written that way: runas /user:CONTOSO\opsadmin "notepad C:\bat\notes.txt". Quote the whole thing rather than guessing where CMD will split it.

An elevated session and a runas session can resolve the same command name to different files. Different account means a different user PATH, and elevation can change it again. If a script works in one window and reports that a tool is missing in the other, the where and Get-Command guide shows how to find out which copy each window is actually running.

A scheduled task is the clean way to get both at once. runas can change the account but not the token, and UAC can change the token but not the account. Task Scheduler does both: a task configured to run as a specific account with the highest run level gets the full token without a prompt and without a credential saved in your own profile. The schtasks guide covers creating one from the command line.

runas failing with a bare “Access is denied” has a documented cause that has nothing to do with your password. Microsoft KB 977513 describes the Secondary Logon service’s own access control list being left wrong by a Windows Server upgrade, which stops a standard user from starting the service at all. The fix is to reset that service’s security descriptor. Check sc qc seclogon before you start doubting the account.


Cross-shell equivalent

PowerShell has both halves of this problem, and it keeps them in two parameter sets that cannot be used together. That turns out to be the clearest statement of the whole article.

Start-Process -Credential is the runas equivalent: it changes the account, and like runas it does not elevate.

# The runas equivalent: run as another account. You are prompted for the password.
# This does NOT elevate, for exactly the same reason runas does not.
$cred = Get-Credential 'SRV-PROD-01\opsadmin'
Start-Process -FilePath 'cmd.exe' -Credential $cred

Start-Process -Verb RunAs is the elevation equivalent: it triggers the UAC prompt and gives you a full token, but it always runs as the account you are already signed in as.

# The elevation equivalent: full token via the UAC prompt, same account as you.
Start-Process -FilePath 'powershell' -Verb RunAs

Ask for both at once and PowerShell refuses before it starts anything, because -Credential belongs to the Default parameter set and -Verb belongs to UseShellExecute. Measured against the parameter metadata of Start-Process:

ParameterParameter set it belongs to
-CredentialDefault
-LoadUserProfileDefault
-NoNewWindowDefault
-VerbUseShellExecute
# Asking for a different account AND elevation in one call.
Start-Process -FilePath 'cmd.exe' -Credential $cred -Verb RunAs

Measured on PowerShell 7.4.6, it throws a ParameterBindingException with the error id AmbiguousParameterSet, and this message:

Parameter set cannot be resolved using the specified named parameters. One or more
parameters issued cannot be used together or an insufficient number of parameters
were provided.
Note: The same split is in the documented syntax for Windows PowerShell 5.1 and PowerShell 7, so this is not a version difference. -LoadUserProfile and -NoNewWindow are in the same set as -Credential, which means -Verb RunAs cannot use those either.

For the elevation check itself, PowerShell does not need to parse anything at all. It can ask the token. Microsoft’s access token reference names CheckTokenMembership as the function that answers whether a SID is enabled in a token, as opposed to merely present, and the .NET method below is the one PowerShell reaches for:

# Asks whether the Administrators SID is ENABLED in this token, not merely present.
# Returns $false inside a runas window and $true in an elevated one.
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($id)
$principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Result: This is the check to use in PowerShell. It reads the token through the API instead of reading text, so none of the tokenizing in the previous section applies to it.

Where this matters

  • A change window that fails at the first command. The maintenance steps were tested in an elevated prompt and run in a runas window, and the first registry or service change fails with an access error that sends everyone looking at NTFS permissions.
  • An older installer that appears to work and installs nothing. Windows virtualizes file and registry writes for 32 bit applications that carry no requested execution level in their manifest, so their writes to protected locations are redirected into the user’s profile rather than failing loudly. Anything elevated, 64 bit or properly manifested is excluded.
  • A remote script that passes in the lab and fails in a branch office. The lab server is domain joined and the branch box is in a workgroup, so the same local administrator account is filtered in one place and not the other.
  • A junior engineer who cannot reproduce your result. You tested with the built-in Administrator account, which is exempt from Admin Approval Mode by default, and they used a named admin account, which is not.
  • An audit finding about cached administrative credentials. /savecred is convenient enough that it spreads, and cmdkey /list on a handful of workstations is usually the fastest way to find out how far.
  • A Group Policy setting that seems to apply and does not. Policy is not processed for a runas account, so anything you see in that window may be left over from that account’s last real sign in.

Tips and limitations

  • runas cannot take a password on the command line, and that is deliberate. Anything that claims to automate the prompt is either /savecred or a third party tool, and both move the credential somewhere it persists.
  • runas runs executables. Microsoft’s own scripting blog documents what happens when you hand it a script file: the error says the file is not a valid Win32 application, which reads as though the script is broken. Call the interpreter instead and pass the script to it as an argument.
  • The Secondary Logon service starts Manual by default. A stopped seclogon is normal and starts on demand. A disabled one breaks runas, Run as administrator and Run as a different user together.
  • /profile and /netonly are mutually exclusive, and so are /savecred and /smartcard. The reference states both pairs.
  • /netonly does not validate the credentials when you type them, so a typo surfaces at the first network call rather than at the prompt.
  • /trustlevel only ever lowers authorization. Run runas /showtrustlevels to see what the local machine offers before using it.
  • Some shell items cannot be started this way at all. The reference names the Printers folder and desktop items, which are opened indirectly.
  • The FilterAdministratorToken and LocalAccountTokenFilterPolicy values in this article are reads. Changing either one changes the security posture of the machine, and the second one is a common finding in security reviews.
  • runas has no current page in the Windows Commands A to Z reference. The live reference is the archived Windows Server 2012 R2 page linked below, whose own text still describes the command in terms of Windows Vista.

Official documentation


Related tools

  • Windows 11 registry tweak generator: builds the registry syntax for values under the Policies key, which is where both FilterAdministratorToken and LocalAccountTokenFilterPolicy live.
  • Windows event log analyzer: useful when a runas attempt fails silently and the only record of it is a logon event in the Security log.

Related guides