Using Windows Credential Manager for API Keys in PowerShell
Command-line clients commonly read API keys from environment variables. Assigning
literal values in $PROFILE makes those keys available in every PowerShell
session:
$env:ANTHROPIC_API_KEY = "sk-ant-api03-..."
$env:OPENAI_API_KEY = "sk-proj-..."
This configuration stores the keys as plaintext in a profile script. It also creates a source-control exposure if the profile is tracked without first removing or externalising the values.
Windows Credential Manager provides per-user storage for generic credentials. The profile can retrieve those credentials at startup and assign them to environment variables only for the lifetime of the PowerShell process. Windows does not expose a native PowerShell cmdlet for reading the credential blob, so the implementation below calls the Win32 Credential Management API directly.
Design Scope
PowerShell Gallery modules such as CredentialManager can wrap the same native
API. The direct-interoperability approach used here removes the module
dependency and keeps the retrieval logic in $PROFILE. It requires Windows,
PowerShell with Add-Type, and permission to access the current user’s
credential set.
Win32 Interoperability
CredRead is exported by advapi32.dll. A C# type compiled through Add-Type
declares the required native structures and Platform Invocation (P/Invoke)
signatures. No PowerShell module or NuGet package is required.
Add the following definitions and assignments to $PROFILE:
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class CredManager {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct CREDENTIAL {
public uint Flags;
public uint Type;
public string TargetName;
public string Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public uint CredentialBlobSize;
public IntPtr CredentialBlob;
public uint Persist;
public uint AttributeCount;
public IntPtr Attributes;
public string TargetAlias;
public string UserName;
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CredRead(string target, uint type, uint flags, out IntPtr credential);
[DllImport("advapi32.dll")]
private static extern void CredFree(IntPtr credential);
public static string Read(string target) {
IntPtr ptr;
if (!CredRead(target, 1, 0, out ptr))
return null;
var cred = (CREDENTIAL)Marshal.PtrToStructure(ptr, typeof(CREDENTIAL));
string secret = Marshal.PtrToStringUni(cred.CredentialBlob, (int)cred.CredentialBlobSize / 2);
CredFree(ptr);
return secret;
}
}
"@ -ErrorAction SilentlyContinue
$env:ANTHROPIC_API_KEY = [CredManager]::Read("ANTHROPIC_API_KEY")
$env:OPENAI_API_KEY = [CredManager]::Read("OPENAI_API_KEY")
$env:MISTRAL_API_KEY = [CredManager]::Read("MISTRAL_API_KEY")
Add-Type registers CredManager in the current PowerShell process. The
-ErrorAction SilentlyContinue option suppresses the duplicate-type error if
the profile is loaded more than once in that process. Separate terminal
processes have independent .NET runtimes and register the type independently.
This option also suppresses compilation diagnostics, so remove it while
troubleshooting the type definition.
Creating Generic Credentials
The built-in cmdkey utility creates generic credential records. Omitting
/pass: causes cmdkey to request the secret interactively instead of placing
it in the command line:
cmdkey /generic:"ANTHROPIC_API_KEY" /user:"API_KEY"
cmdkey /generic:"OPENAI_API_KEY" /user:"API_KEY"
The target names must match the strings passed to [CredManager]::Read. The
reader ignores the stored username and consumes only the credential blob, so
API_KEY is used as descriptive metadata. Supplying a literal /pass: value
is not recommended because the key may be retained in shell history or exposed
through command-line inspection.
The same records can be created through Credential Manager by selecting Windows Credentials and then Add a generic credential.
Retrieval Sequence
The C# shim performs four operations:
CredReadrequests a generic credential (CRED_TYPE_GENERIC, value1) by target name and returns a pointer to an allocatedCREDENTIALstructure.Marshal.PtrToStructuremaps the unmanaged structure to the managedCREDENTIALdeclaration.Marshal.PtrToStringUniinterprets the application-defined credential blob as UTF-16.CredentialBlobSizeis expressed in bytes, so the code divides it by two to obtain the character count.CredFreereleases the buffer allocated byCredRead.
If CredRead fails, Read returns null. Assigning that result to an
environment variable leaves no usable key in the session.
Security Boundary
Credential Manager protects the stored value at rest and associates the
credential set with the current Windows logon context. This removes plaintext
keys from $PROFILE, source control, and ordinary file storage.
It does not protect a key from code already running as the same user. After profile initialization, each key exists as a managed string and as a process environment variable. Child processes can inherit it, and processes with sufficient access to the session may be able to inspect it. This design addresses storage and accidental-disclosure risks; it is not an isolation boundary against a compromised user account or process.
Verification
Reload the profile with . $PROFILE, or start a new PowerShell process, and
verify only a short prefix:
$env:ANTHROPIC_API_KEY.Substring(0, 12) + "..."
# sk-ant-api0...
Do not write the complete value to the terminal, transcript, or diagnostic log.
Comparison with User Environment Variables
Persistent user environment variables are stored under HKCU\Environment.
Using the Windows environment-variable settings therefore moves the plaintext
value from $PROFILE to the registry without adding encrypted storage.
Comparison with .env Files
A .env file separates configuration from the profile and can be excluded with
.gitignore, but its contents remain plaintext on disk. File permissions can
restrict access to other accounts; they do not prevent reads by processes
running under the owning account.
Windows Credential Manager is appropriate when the objective is to remove long-lived plaintext keys from scripts and files while retaining environment-variable compatibility. Workloads that require central rotation, access policy, auditing, or protection from the interactive user context require a dedicated secret-management service instead.