The Symptom
A call that used to just work:
sqlcmd -S SQL01 -q "SELECT @@VERSION"
...now fails with an anonymous-logon error, even though the account running it is the same domain user that always worked, with the same rights on the same instance. Re-running it with an explicit domain login makes no difference — the account was never the problem.
What Actually Changed
Microsoft replaced the classic ODBC-based sqlcmd.exe with a full rewrite in Go ("go-sqlcmd"). It ships automatically with newer SQL Server Tools installs and with Azure Data Studio — installing either one silently puts the new binary ahead of the old one on PATH.
| Aspect | Old sqlcmd (ODBC) | New go-sqlcmd |
|---|---|---|
| Call with no auth parameters | Falls back to Windows Authentication (SSPI) automatically | Sends no credentials — connects as NT AUTHORITY\ANONYMOUS LOGON |
| Result | Connects with the current Windows identity | SQL Server rejects the anonymous connection |
| Fix | Not needed | Pass -E explicitly to request Windows Authentication |
The old binary's implicit SSPI/NTLM fallback is simply gone in the rewrite. It isn't a bug in the SQL Server engine, a permission change, or a Kerberos regression — it's a behavior change in the client tool itself, and it hits hardest exactly where nobody is watching: unattended jobs, scheduled tasks, and internal tooling that call bare sqlcmd without ever having needed an auth flag before.
NT AUTHORITY\ANONYMOUS LOGON looks exactly like what you'd see if a Service Principal Name were missing — but it isn't the same failure. The quick way to tell them apart: run the exact same call once with -E added. If -E makes it work immediately, it's this client-side regression, not a directory-level SPN gap. Real background on the SPN side of Windows Authentication is in Kerberos and SPN: Windows Authentication Deep Dive, including Get-sqmSPNReport for checking whether SPNs are actually the issue.
The One-Line Fix (and Why It Doesn't Scale)
Adding -E to any bare call resolves it immediately:
sqlcmd -S SQL01 -E -q "SELECT @@VERSION"
That's the whole fix, technically. The problem is where that call happens: a scheduling tool, a deployment pipeline, dozens of ad-hoc scripts written over years, all calling plain sqlcmd because that always worked. Editing every call site is a real project — and every call site you miss keeps failing the same way after the next SQL Server Tools or Azure Data Studio update.
A Transparent Wrapper Instead
Rather than touching every caller, a small wrapper can sit in front of the real sqlcmd.exe on the system PATH: it inspects the arguments it was called with, adds -E only when nothing else already authenticates the call, and passes everything through untouched otherwise. Callers keep invoking sqlcmd exactly as before — they never know the wrapper is there.
Two files make up the wrapper:
sqlcmd.bat— the entry point that actually sits onPATH. It forwards every argument to the PowerShell script and passes the exit code straight back, so error handling in the calling script or tool keeps working unchanged.sqlcmd_wrapper.ps1— the decision logic.
@echo off
REM Transparent wrapper for go-sqlcmd. Adds -E (Windows Auth) when needed.
setlocal enabledelayedexpansion
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0sqlcmd_wrapper.ps1" %*
exit /b !ERRORLEVEL!
The decision itself is a short check: does the call already carry a SQL login (-U) or any explicit auth flag (-E, -A, -N, -P)? If yes, pass everything through unchanged — the caller knew what it wanted. If neither is present, insert -E:
function Add-WindowsAuthIfNeeded {
if ($hasUserLogin) { return @($Arguments) } # -U present: leave alone
if ($hasAuthFlag) { return @($Arguments) } # -E/-A/-N/-P present: leave alone
return @("-E") + @($Arguments) # neither: add Windows Auth
}
The trickier part is finding the real sqlcmd.exe without the wrapper calling itself in a loop. It checks, in order: the system PATH (skipping its own install folder), a list of known install locations for SQL Server Tools 2016–2022 and Azure Data Studio, and finally where.exe as a last resort filtered to exclude the wrapper's own directory.
Installing It
📦 Download the ready-to-run installer
A small ZIP with the wrapper plus a one-click installer that does the whole setup below automatically, including the PATH change:
⬇ Download sqlcmd-wrapper-setup.zipExtract it anywhere and double-click Install-SqlcmdWrapper.bat. It asks for the one Administrator (UAC) prompt it needs — writing to the machine-wide PATH requires elevation — then copies the two wrapper files to C:\Tools\sqlcmd-wrapper and prepends that folder to the system PATH (not the user PATH). Open a new terminal afterward (existing shells keep the old PATH cached) and verify:
where sqlcmd
sqlcmd -S SERVERNAME -q "SELECT @@VERSION"
The first line from where sqlcmd must now be C:\Tools\sqlcmd-wrapper\sqlcmd.bat, and the query should succeed with Windows Authentication, no flag needed. To remove everything again, double-click Uninstall-SqlcmdWrapper.bat from the same ZIP — it reverses both steps and deletes the folder.
.ps1 files are plain, readable PowerShell (no compiled binary, no obfuscation) — open them in a text editor first if you want to see exactly what runs before you approve the prompt.
Doing it by hand instead
The installer only automates these five steps — worth knowing if you'd rather run them yourself, or need a different target folder:
- Create
C:\Tools\sqlcmd-wrapperand copysqlcmd.bat+sqlcmd_wrapper.ps1into it (both files must be in the same folder). - Prepend that folder to the system
PATH(not the userPATH) — it has to come before the realsqlcmd.exe's location. - Open a new terminal. Existing shells keep the old
PATHcached and won't see the change. - Verify with
where sqlcmd— the first line returned must beC:\Tools\sqlcmd-wrapper\sqlcmd.bat. - Test:
sqlcmd -S SERVERNAME -q "SELECT @@VERSION"should now succeed with Windows Authentication, no flag needed.
To remove it by hand, drop the folder from the system PATH, open a new terminal, and the real sqlcmd.exe is called directly again — nothing else was ever changed.
Debugging a Bad Install
If it's still not behaving, turn on the wrapper's own verbose logging before touching anything else:
$env:SQLCMD_WRAPPER_DEBUG = "1"
sqlcmd -S SERVERNAME -q "SELECT @@VERSION"
This prints which real sqlcmd.exe the wrapper resolved to and whether it decided to insert -E — almost always enough to spot a stale PATH entry or a terminal that was never reopened after the install.
Best Practices
- ☐ Put the wrapper folder at the front of the system
PATH, not the userPATH— scheduled tasks and services run under accounts that only see the machine-wide value. - ☐ Always open a brand-new terminal after any
PATHchange — running shells and already-started services keep their old copy. - ☐ If
-Edoesn't fix it, stop suspecting the wrapper and check for a real SPN gap instead — see Kerberos and SPN: Windows Authentication Deep Dive. - ☐ Re-check
where sqlcmdafter every SQL Server Tools or Azure Data Studio update — a reinstall can add a newsqlcmd.exepath ahead of the wrapper. - ☐ Treat the wrapper as a transitional shim, not a permanent fix — where you control the calling code, add
-Eexplicitly over time and retire the dependency on it. - ☐ Keep the exit-code passthrough intact if you ever customize the wrapper — automation depends on it for error handling, not just on the output text.
- ☐ Roll it out to more than one machine with the installer, not copy-paste —
Install-SqlcmdWrapper.batdoes the folder, files, and PATH change identically every time.
The Bottom Line
This isn't a SQL Server problem, and it isn't a Kerberos problem — it's a client tool that quietly dropped a fallback it used to have, surfacing as an access-denied error that looks exactly like a permissions issue. Adding -E fixes any single call instantly; a transparent wrapper on PATH fixes every call that would otherwise need it, without opening up every script that calls sqlcmd to make the same one-character edit.