CodexArsenal is a PowerShell research module for communicating directly with OpenAI's backend APIs.
These APIs are not guaranteed to work as OpenAI could change them over time.
- Windows PowerShell 5.1 or later
- A ChatGPT account with Codex access
git clone https://github.com/jonny-jhnson/CodexArsenal.git
Import-Module .\CodexArsenal.psd1 -ForceRun Get-Help <cmdlet> -Full for parameters and examples.
The easiest way to obtain an access token is locally by looking at .codex\auth.json or you can run Get-CodexTokens if you have a refresh token:
$tokens = Get-CodexTokens -RefreshToken $refreshToken
$accessToken = $tokens.AccessToken
$accountId = '<your-account-id>'If you don't have .codex\auth.json I built Invoke-CodexLogin:
Normal access token (openid profile email offline_access api.connectors.read api.connectors.invoke):
$login = Invoke-CodexLogin
$accessToken = $login.AccessToken
$accountId = $login.AccountId$stepUp = Invoke-CodexLogin `
-RemoteControlEnrollment `
-AccountId $accountIdList all tools available through the WHAM apps MCP surface, then call one.
Get-CodexTool and Invoke-CodexTool create a new MCP session automatically when -SessionId
is omitted.
# List available tools
Get-CodexTool -AccessToken $accessToken -AccountId $accountId |
Select-Object name, description
# Call a tool
Invoke-CodexTool -AccessToken $accessToken -AccountId $accountId `
-Name gmail_send_email -Arguments @{
to = 'someone@gmail.com'; subject = 'Hi'; body = 'From Codex'
}Create one session and pass its ID to each command when making multiple MCP requests:
$session = Connect-CodexMcp `
-AccessToken $accessToken `
-AccountId $accountId
Get-CodexTool `
-AccessToken $accessToken `
-AccountId $accountId `
-SessionId $session |
Select-Object name, description
Invoke-CodexTool `
-AccessToken $accessToken `
-AccountId $accountId `
-SessionId $session `
-Name gmail_send_email `
-Arguments @{
to = 'someone@gmail.com'
subject = 'Hi'
body = 'From Codex'
}Invoke-CodexMcp is the low-level JSON-RPC transport underneath Get-CodexTool and
Invoke-CodexTool. Use it directly if you need to call arbitrary MCP methods.
The rogue server scenario is broken down into two pieces - server and controller enrollment. The server registers an environment and listens for requests. The controller enrolls its own identity, claims the server's pairing code, and sends commands through OpenAI's remote-control relay. Both sides make outbound connections and do not need direct network access to each other.
$reg = Register-CodexRemoteServer `
-AccessToken $accessToken `
-AccountId $accountId `
-Name 'RogueVM'
$pair = Start-CodexRemotePairing -ServerEnrollment $reg
$pair | Format-List ManualPairingCode, EnvironmentId, ExpiresAt
$conn = Connect-CodexRemoteServer `
-RemoteControlToken $reg.RemoteControlToken `
-ServerId $reg.ServerId `
-InstallationId $reg.InstallationId `
-Name $reg.Name
Start-CodexRogueServer -Connection $conn -DurationSeconds 300 -VerboseSend the short-lived ManualPairingCode to the controller over a separate initial transport.
The controller requires a normal login and a separate remote-control enrollment login. Both must use the same ChatGPT user and workspace.
$login = Invoke-CodexLogin
$accessToken = $login.AccessToken
$accountId = $login.AccountId
$stepUp = Invoke-CodexLogin `
-RemoteControlEnrollment `
-AccountId $accountId
$keyPath = Join-Path $env:USERPROFILE '.codex-arsenal\keys\rogue-controller-key.json'
$client = Register-CodexRemoteClient `
-AccessToken $accessToken `
-AccountId $accountId `
-StepUpToken $stepUp.AccessToken `
-KeyPath $keyPath
$grant = Complete-CodexRemotePairing `
-AccessToken $accessToken `
-AccountId $accountId `
-ClientEnrollment $client `
-ManualPairingCode '<code from server>'
$ctl = Connect-CodexRemoteController `
-AccessToken $accessToken `
-AccountId $accountId `
-EnvironmentId $grant.Response.environment_id `
-ClientEnrollment $clientThe controller key file contains exportable private-key material. Treat it like a credential and remove it when done using Remove-Item $keyPath.
If the controller was previously enrolled but its remote-control token has expired, refresh it using the existing client enrollment and device key. This avoids repeating the step-up enrollment:
$client = Update-CodexRemoteClient `
-ClientEnrollment $client `
-AccessToken $accessToken `
-AccountId $accountIdReconnect with Connect-CodexRemoteController after refreshing the client token.
Send commands:
# Current identity on the server
(Send-CodexRemoteCommand `
-Connection $ctl `
-Method 'WhoAmI' `
-TimeoutSeconds 10).Reply.result.output
# Server and environment information
(Send-CodexRemoteCommand `
-Connection $ctl `
-Method 'GetInfo').Reply.result
# Execute a command
(Send-CodexRemoteCommand `
-Connection $ctl `
-Method 'Exec' `
-Params @{ command = 'hostname' }).Reply.result
# Download a small file from the server
$download = (Send-CodexRemoteCommand `
-Connection $ctl `
-Method 'DownloadFile' `
-Params @{ path = 'C:\server\file.txt' }).Reply.result
[System.IO.File]::WriteAllBytes(
'C:\client\file.txt',
[Convert]::FromBase64String($download.contentBase64)
)
# Upload a small file to the server
$contentBase64 = [Convert]::ToBase64String(
[System.IO.File]::ReadAllBytes('C:\client\file.txt')
)
(Send-CodexRemoteCommand `
-Connection $ctl `
-Method 'UploadFile' `
-Params @{
path = 'C:\server\file.txt'
contentBase64 = $contentBase64
}).Reply.resultFile transfer (upload and download) is single-frame and intended for files around 100 KB or less.
$login = Invoke-CodexLogin
$reg = Update-CodexRemoteServer `
-ServerEnrollment $reg `
-AccessToken $login.AccessToken `
-AccountId $login.AccountIdThis refreshes the server token without replacing the existing server or environment. The server's ServerId and InstallationId must still be available.
Remove-CodexRemoteDevice `
-AccessToken $login.AccessToken `
-AccountId $login.AccountId `
-EnvironmentId $reg.EnvironmentIdUse -WhatIf to preview the removal without deleting the enrollment.
Jonathan Johnson (@JonnyJohnson_)