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:

  1. CredRead requests a generic credential (CRED_TYPE_GENERIC, value 1) by target name and returns a pointer to an allocated CREDENTIAL structure.
  2. Marshal.PtrToStructure maps the unmanaged structure to the managed CREDENTIAL declaration.
  3. Marshal.PtrToStringUni interprets the application-defined credential blob as UTF-16. CredentialBlobSize is expressed in bytes, so the code divides it by two to obtain the character count.
  4. CredFree releases the buffer allocated by CredRead.

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.