Stop Hardcoding API Keys in Your PowerShell Profile


If you work with AI coding agents on Windows, you’ve probably got a handful of API keys that need to be available as environment variables in every terminal session. The straightforward approach is to put them directly into your $PROFILE:

$env:ANTHROPIC_API_KEY = "sk-ant-api03-..."
$env:OPENAI_API_KEY = "sk-proj-..."

This works, but the secrets then live in a plain text file on disk. Any process running as your user can read them, and if your dotfiles are in a Git repo there’s an extra risk of accidentally committing the file.

macOS has the security command, which reads secrets from the system Keychain in a single line you can pipe into an environment variable. On Windows, the equivalent store is the Credential Manager — the same place that holds saved Wi-Fi passwords and browser credentials — but there’s no built-in CLI to query it from PowerShell.

Existing PowerShell Modules

The first instinct is to install a PowerShell module. The Gallery has a few options with names like CredentialManager or CredentialManagement. Coverage and maintenance vary: some haven’t been updated in a while, some only target Windows PowerShell 5.1, and behaviour under pwsh 7 isn’t always reliable. After trying a couple of them, I went looking for a smaller alternative.

Calling the Win32 API Directly

The Windows API for reading credentials is CredRead in advapi32.dll. It’s been stable since Windows XP, and you can call it directly from PowerShell via Add-Type with a small C# shim — no modules, no NuGet packages, no dependencies beyond what ships with Windows.

Here’s the complete solution. Drop this into your $PROFILE (edit it with code $PROFILE or notepad $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")

The -ErrorAction SilentlyContinue on Add-Type is important: without it, opening a second terminal tab would fail because the CredManager type is already loaded in the current process and .NET refuses to define it again.

Storing Credentials

Before the profile can read anything, you need to put your keys into the Credential Manager. The built-in cmdkey command does this:

cmdkey /generic:"ANTHROPIC_API_KEY" /user:"heiko" /pass:"sk-ant-api03-..."
cmdkey /generic:"OPENAI_API_KEY" /user:"heiko" /pass:"sk-proj-..."

The /user: value doesn’t matter for this use case — the code only reads the password blob. Pick whatever makes sense when you see it listed in the Credential Manager UI.

You can also add credentials through the GUI: open “Credential Manager” from the Start menu, select “Windows Credentials”, and click “Add a generic credential”.

How It Works

The C# shim does three things:

  1. CredRead looks up a “Generic Credential” (type 1) by its target name and returns a pointer to a CREDENTIAL struct in unmanaged memory.
  2. Marshal.PtrToStructure copies the unmanaged struct into a managed C# object so we can read its fields.
  3. Marshal.PtrToStringUni reads the credential blob — which is stored as a UTF-16 byte array — into a .NET string. The blob size is in bytes, so we divide by 2 to get the character count.
  4. CredFree releases the unmanaged memory that CredRead allocated.

The credentials are stored encrypted under your Windows user profile using DPAPI. They can only be decrypted by processes running as your user account on your machine. This is the same protection that Windows uses for saved passwords in Edge, RDP connections, and mapped network drives.

Verifying It Works

After reloading your profile (. $PROFILE or just open a new terminal), check that the variables are set:

$env:ANTHROPIC_API_KEY.Substring(0, 12) + "..."
# sk-ant-api0...

Don’t echo the full key — the point of all this is to keep it out of plain text.

Why Not Use $env: in the System Settings?

You could set environment variables through the Windows system UI (“Edit environment variables for your account”). The problem: those values are stored in the registry in plain text, visible to any process that can read HKCU\Environment. You’re trading one plain text location for another.

Why Not Use a .env File?

A .env file has the same drawback: it’s plain text on disk. It is somewhat better than hardcoding in your profile because you can .gitignore it, but it’s still unencrypted and readable by anything running as your user.

The Credential Manager approach gives you OS-level encryption at rest, per-user isolation, and a stable Windows API that should keep working across PowerShell and Windows versions. For roughly 30 lines of C# pasted once into your profile, that seems like a reasonable trade.