NetSuite · SuiteCloud CLI · 2 November 2026

Deploying SDF from PowerShell without the browser login

The SuiteCloud CLI’s browser login expires and needs a person at the keyboard. Certificate-based machine-to-machine auth doesn’t. Here’s the full setup on Windows — including the error almost everyone hits on the first try.

The usual way to connect the SuiteCloud CLI to an account is account:setup: a browser window opens, you sign in, you click Allow. It works until the token expires, and then the next deploy stops and waits for someone to sign in again. That’s fine at a desk and useless for a scheduled job, a script, or an automated pipeline.

The alternative is account:setup:ci, which uses OAuth 2.0 client credentials with a certificate. No browser, no expiring session. The setup takes about fifteen minutes. These steps are for Windows and PowerShell; the CLI parts are the same anywhere.

1. Generate the key pair

Git for Windows ships OpenSSL, so you probably have it already. Use an RSA key; elliptic-curve keys are a common cause of private-key errors at the next step.

$ossl = "C:\Program Files\Git\mingw64\bin\openssl.exe"
$dir  = "$env:USERPROFILE\.suitecloud-m2m"
New-Item -ItemType Directory -Force $dir | Out-Null

& $ossl req -new -x509 -newkey rsa:4096 -sha256 -nodes -days 730 `
  -subj "/CN=SuiteCloud CLI M2M" `
  -keyout "$dir\m2m_private.pem" -out "$dir\m2m_public.pem"

# Only your account (and SYSTEM) can read the folder
icacls $dir /inheritance:r /grant:r "$($env:USERNAME):(OI)(CI)F" "SYSTEM:(OI)(CI)F"

Keep the folder out of OneDrive or any other synced location. The private key is a deploy credential.

2. Map the certificate in NetSuite

Go to Setup > Integration > Manage Authentication > OAuth 2.0 Client Credentials (M2M) Setup and create a new mapping:

Save, and copy the Certificate ID it shows. If the account rejects M2M entirely, check that OAuth 2.0 and SuiteCloud Development Framework are enabled under Enable Features.

3. The error everyone hits

Run account:setup:ci in a normal PowerShell window and you’ll most likely get:

Machine-to-machine authentication is not allowed for the current execution context.

The CLI decides what kinds of authentication it allows from two environment variables, and with neither set it’s in browser mode:

The catch: auth IDs created in browser mode aren’t visible in machine-to-machine mode, and the reverse. Set these variables machine-wide and your normal browser login, the one your IDE uses, stops working.

4. Store the passkey properly, and register the auth ID

Generate a random passkey once and store it encrypted with Windows DPAPI, so only your Windows login can read it:

$chars = [char[]]'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
$bytes = New-Object byte[] 64
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
$pk = -join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] })
ConvertTo-SecureString $pk -AsPlainText -Force |
  ConvertFrom-SecureString | Set-Content "$dir\ci_passkey.dpapi"

# Setup context: passkey only
$env:SUITECLOUD_CI_PASSKEY = $pk
suitecloud account:setup:ci --account 1234567 --authid PROD-CI `
  --certificateid "<certificate ID from step 2>" `
  --privatekeypath "$dir/m2m_private.pem"
Remove-Item Env:SUITECLOUD_CI_PASSKEY

5. A wrapper that turns CI mode on for one command

Rather than setting the variables globally, run CLI commands through a small script that sets them for its own process and clears them afterwards:

# sdf-ci.ps1 - run one SuiteCloud command in machine-to-machine mode
param([Parameter(ValueFromRemainingArguments=$true)][string[]]$CliArgs)
$enc = Get-Content (Join-Path $PSScriptRoot 'ci_passkey.dpapi') |
  ConvertTo-SecureString
$env:SUITECLOUD_CI_PASSKEY = [Runtime.InteropServices.Marshal]::PtrToStringBSTR(
  [Runtime.InteropServices.Marshal]::SecureStringToBSTR($enc))
$env:SUITECLOUD_CI = '1'
try { & suitecloud @CliArgs }
finally {
  Remove-Item Env:SUITECLOUD_CI, Env:SUITECLOUD_CI_PASSKEY -ErrorAction SilentlyContinue
}

Point the project at the new auth ID by setting defaultAuthId to PROD-CI in its project.json, then from the project folder:

& "$env:USERPROFILE\.suitecloud-m2m\sdf-ci.ps1" project:validate --server
& "$env:USERPROFILE\.suitecloud-m2m\sdf-ci.ps1" project:deploy

The browser-based auth ID keeps working in the IDE, because the CI variables never exist outside that one command.

Finish the loop: commit and push the project after every successful deploy. When deploys no longer need a person in the browser, the repository is the only record of what went into the account and when.

Housekeeping

Related: four SDF deploy errors and what they actually mean, and SuiteScript 2.1 before the deadline.

New England Systems Group

If there's a process everyone works around instead of through, that's where we start.

SuiteScript development and back-office automation for manufacturers, distributors, producers and retailers. Danbury, Connecticut.

hello@nesystemsgroup.com