GitHub Actions
Exchange your GitHub Actions OIDC token for a short-lived credential from us and run one step with the values.
Not verified yet
Your workflow exchanges a GitHub-signed OIDC token for a credential we mint. You store no long-lived value in repository settings. See machine identity and values in a CI build.
Trust a GitHub repository
Open Machine Identities then Connect a Platform and pick GitHub Actions. The dialog has no subject field: we read the subject from the GitHub API, because repositories created or renamed since July 2026 embed numeric ids and a typed subject would never match.
The first time, Connect sends you to GitHub to install the Penv Cloud app, then brings you back to pick a repository. Installing the app creates no identity; picking a repository does. If the app was already installed on your account, GitHub has nothing to change and doesn't send you back: click Already installed? Check again on the card instead. Repositories the app covers with no identity yet are listed under Ready to connect on All Identities.
Connect a platform and the GitHub Actions trust reference list what the trust holds.
The audience is your workspace id, shown in the console.
Run penv in the workflow
permissions:
id-token: write
steps:
- name: Install penv
run: |
curl -fsSL https://penv.cloud/install | sh
echo "$HOME/.penv/bin" >> "$GITHUB_PATH"
- name: Deploy
run: |
T=$(curl -sS \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=YOUR_WORKSPACE_ID" | jq -r .value)
C=$(T="$T" jq -n '{token:env.T}' | curl -sS "https://penv.cloud/api/v1/auth/oidc" \
-H 'content-type: application/json' --data-binary @- | jq -r .credential)
PENV_TOKEN="$C" penv run -- npm run deploypermissions and the token request are GitHub's
OIDC mechanism.
| Line | Note |
|---|---|
>> "$GITHUB_PATH" | The installer edits no startup file; this puts penv on PATH for later steps |
jq | Both curl calls parse with jq; install it on your runners that lack it |
PENV_TOKEN="$C" | Takes precedence over the keychain. A runner has no keychain, so every run is online and nothing is cached |
| Credential | Lasts fifteen minutes, capped by the trust's expiry. Keep both tokens inside one step |
For a host that needs a file, PENV_TOKEN="$C" penv pull writes a plain .env in the build step.
See penv run, penv pull and
environment variables.
Published action
The action has no uses: coordinate yet. Use the workflow above until it is published; this page
stays needs-source until then. Once published, reference it by full commit SHA
(why).
The action does the same exchange without installing penv. It is a JavaScript action on node24
with a main step and a post step on always(), so a failed build revokes the credential too.
It reads none of your repository secrets and mints its token per run.
| Input | Required | Value |
|---|---|---|
audience | yes | The workspace id |
run | yes | The command that receives the values; no other step does |
api-origin | no | Defaults to https://penv.cloud |
files | no | One NAME=path/name per line, for a tool that cannot read a variable |
A files entry writes the value to a 0600 file under $RUNNER_TEMP, sets NAME to its path,
and removes the file before the step ends, including on failure.
The action fails the step, naming the parameter, when a derived name or a files name would set a
variable that changes how your command runs rather than what it reads. Rename the parameter.
| Refused | Examples |
|---|---|
| Loader and libc | LD_*, DYLD_*, GCONV_PATH, GLIBC_TUNABLES, MALLOC_*, TMPDIR |
| Shell | PATH, HOME, SHELL, ENV, BASH_*, SHELLOPTS, BASHOPTS, PS1-PS4, PROMPT_COMMAND, IFS, CDPATH |
| Interpreters | NODE_OPTIONS, NODE_PATH, PYTHON*, PERL*, RUBYOPT, RUBYLIB, JAVA_TOOL_OPTIONS, _JAVA_OPTIONS, JDK_JAVA_OPTIONS, *_OPTS of Java build tools, DOTNET_STARTUP_HOOKS, CORECLR_* |
| Toolchains | GIT_*, NPM_CONFIG_*, GOFLAGS, GOPROXY, RUSTC_WRAPPER, CARGO_*, CC, CXX, MAKEFLAGS, OPENSSL_CONF |
| Package managers | PIP_*, PIPX_*, UV_*, POETRY_*, CONDA_*, YARN_*, PNPM_*, BUNDLE_*, GEM_*, COMPOSER_*, NUGET_*, RUSTUP_* |
| Launched programs | EDITOR, VISUAL, PAGER, LESSOPEN, SSH_ASKPASS, SSH_AUTH_SOCK, GH_HOST |
| Runner and penv | GITHUB_*, RUNNER_*, ACTIONS_*, INPUT_*, STATE_*, PENV_* |
A name that is not a valid variable name is refused too. The command also does not inherit
ACTIONS_ID_TOKEN_REQUEST_*, ACTIONS_RUNTIME_TOKEN, GITHUB_STATE, GITHUB_ENV,
GITHUB_OUTPUT or GITHUB_PATH: it cannot mint another OIDC token or pass a value to a later step.
| Behavior | Detail |
|---|---|
| Credential lifetime | Five minutes, bound to the repository, workflow ref, executing job's workflow ref, run id, run attempt, and actor (audit only) |
pull_request_target | We refuse it |
pull_request, workflow_run | Allowed |
| Runners | Linux and macOS. The action refuses Windows: neither 0600 nor bash holds there |
Values in a CI build covers trigger rules, closed output channels, masking coverage, and what penv cannot promise inside your shared job.
Troubleshooting
| Answer | Meaning |
|---|---|
401 unauthorized | Your trust does not name this subject |
All codes: our API answers.