Docs
Referencesince 1.0.0-alpha.3

Error codes

Every code penv and penv.cloud can answer with, the exit code each one maps to, and the four error shapes you can meet.

Four things answer you with a code. The command line, the penv.cloud API, the endpoint your identity provider writes to, and the AWS connect callback. Each has its own shape, and each shape is below with the codes it can carry.

What penv prints

A refusal is one object on stderr, with the same three members every time.

{
  "error": "no_credential",
  "message": "penv has no credential for this host.",
  "fix": "Run penv login, or set PENV_TOKEN."
}

message says what happened. fix says the one thing to do next. A refusal that has to be replayed carries what the replay needs beside those three, so approval_required also carries approval, url and expiresAt.

At a terminal the same refusal reads as one line: error:, the message, then the fix. The JSON form is what you get when stdout is not a terminal, when you pass --json, or when penv detects an agent.

Exit codes

CodeNameWhat it means
0okThe command did what it says.
1errorSomething went wrong.
2authNot signed in, or the credential was rejected.
3validationThe schema or the values did not pass.
4confirmationA person has to confirm. The JSON carries the command to replay.
5no_credentialNo credential is available and none can be obtained.
6environment_refusedThis identity may not read that environment.

penv run is the exception worth knowing: once the child starts, penv leaves with the child's exit code rather than its own.

Every code the command line answers with

errorExitWhat happened
agent_session2login, pull or push --prune was run under an agent session.
ambiguous1The workspace and project name more than one thing on the server.
ambiguous_output1--output names one file and more than one language target applies.
approval_denied2A person refused the reveal in the console.
approval_expired4The ten minutes ran out before the id was redeemed. Ask for a new one.
approval_mismatch4The approval id covers another key.
approval_redeemed4That id was already spent, and each one prints a value once.
approval_required4A person has to approve this reveal. The id and the page are in the answer.
bad_url1PENV_URL is not an address penv will send a credential to.
binary_lost1An upgrade left no binary in place. Reinstall.
checksum_mismatch1A download is not the file its checksum names. Nothing was replaced.
child_failed1The command penv run started could not be waited for.
cloned2This host's key was seen on another machine, so the identity is locked.
cloud_failed1The client failed in a way with no better name.
denied2The device-code sign-in was denied in the console.
dynamic3That key is minted by an engine. Edit it in the console.
empty_value3penv set was given nothing to write.
environment_refused6This identity may not read that environment, or local mode was asked for one it does not have.
expired2The login expired, or the device code expired before anyone approved it.
guard_failed3A harness file or a guard folder could not be merged. penv hook answers 1.
invalid_key3The key name is not upper snake case.
invalid_schema3.env.schema does not parse. Run penv check for the list.
keychain1The operating system keychain would not open.
managed_install1npm, Homebrew, Nix, winget or Scoop owns this binary.
missing_asset1The release carries no asset for this machine.
no_approval4The approval id is not on this server. Ask for a new one.
no_checksum1The checksum file lists no digest for this asset.
no_command1penv run was given nothing to run.
no_credential5This host can prove nothing. Run penv login, or set PENV_TOKEN.
no_current_exe1penv could not read its own path.
no_echo_off1The terminal will not stop echoing, and a value is never shown.
no_keychain1An enrolled key would not survive the process on this host.
no_org1This account is in no workspace yet.
no_release1There is no release to read.
no_schema1No .env.schema here or in any directory above. Run penv init.
no_secret1penv machine enroll needs the one-time secret from the console.
no_such_org1This account is in no workspace by that name.
no_value3That key has no value in that environment.
not_cloud1.env.schema names no cloud project. Run penv push to create one.
not_found1That project or environment is not on this server.
offline5The server could not be reached, and only a cached development environment runs offline.
org_required1This account is in several workspaces. Name one with --org.
output_outside_repo3The generated file would land outside the repository.
quota_exceeded1The plan's project limit is reached.
rate_limited1The server, or the release host, is throttling this identity.
release_unreachable1The release address could not be read.
schema_exists3.env.schema is already there. Use penv init --force to write it again.
server_error1The server answered 5xx twice.
server_refused1The server answered a status penv has no better name for.
signature_invalid1The release checksum file is not signed by a penv release key.
signature_missing1The release carries no signature for its checksum file.
spawn_failed1The command penv run was given could not be started.
target_failed3A language target folder would not load or render.
unauthorized2The credential is not accepted by this server.
unexpected_answer1The server said nothing had changed for a read that asked for everything.
unknown_command1penv help was given a command penv does not have.
unknown_harness1penv has no guard for that harness.
unknown_key3penv check was given a key the schema does not declare.
unknown_shell1penv writes no completion script for that shell.
unknown_target1No language target by that name, or no release build for this machine.
unreadable_file1A file could not be read.
unreadable_input1A value could not be read from stdin or the terminal.
unreadable_release1The release address answered something that is not a release.
unreadable_selection1An answer to a prompt was not one of the choices.
unsigned_build1This build carries no release key, so it cannot upgrade at all.
unwritable_binary1The binary could not be replaced. Nothing changed.
unwritable_file1A file could not be written.
unwritable_value1A value cannot be written into a .env in any portable way.
value_on_the_command_line1penv set takes no --value, because the shell would keep it in its history.

bad_url covers four shapes of PENV_URL. penv accepts https anywhere, and plain http only for 127.0.0.1, localhost and [::1]. It refuses plain http to any other host, an address carrying a user in front of its host, an address naming no host at all, and any scheme other than those two.

The refusals penv upgrade can answer with are on upgrade penv, with what to do about each one.

What penv.cloud answers

Every route under /api/v1 answers JSON, and an error body is { "error": "<code>" }. The two schema-shaped refusals add a detail member saying which part of the body was wrong.

StatusCodeWhat it means
400name_requiredThe request names no key.
400name_invalidThe key name is not a usable name.
400key_invalidThe key an approval was asked for is not a usable name.
400keys_requiredA push carried no keys array.
400address_requiredAn approval request named no workspace, project and environment.
400body_requiredA write carried no body.
400value_or_schema_requiredA key write carried neither a value nor a schema.
400value_must_be_a_stringThe value in the body is not a string.
400schema_invalidThe per-key schema is not the shape penv schema --json emits.
400token_requiredAn exchange carried no platform token.
400signed_request_requiredThe AWS exchange carried no signed request.
400signed_challenge_requiredThe keypair exchange carried no signed challenge.
400credential_id_requiredA challenge was asked for without a credential id.
400enrollment_requiredAn enrollment carried no secret or no public key.
400ttl_must_be_an_integerA lease asked for a lifetime that is not a whole number of seconds.
401expiredThe credential is past its expiry. Run penv login again.
401unauthorizedThe credential is not accepted. Revoked and unknown read the same.
401replayedA keypair challenge nonce was used twice.
403forbiddenThe credential is real and does not carry this permission or this environment.
403deniedA person denied the device-code sign-in.
403refusedA delivery exchange came from a workflow trigger the workspace does not trust.
403lease_api_disabledLeases are switched off for this environment.
404not_foundNo such project, environment, key or approval, for you.
409dynamicThe key is minted on request, so it cannot be written or read as a stored value.
409clonedThe keypair generation counter does not match. The identity is revoked.
409quota_exceededThe plan's project limit is reached.
409ambiguousThe workspace or project name resolves to more than one thing.
409engine_unavailableThe engine behind a dynamic key would not answer.
410expiredThe device code expired before anyone approved it.
428authorization_pendingNobody has approved the device code yet. Keep polling.
429rate_limitedAn IP or plan limit. retry-after says how long.
429slow_downYou are polling the device code faster than the interval.
429ceiling_reachedThe abuse ceiling on dynamic leases.
500mint_failedThe engine accepted the request and produced nothing.
503unavailableA dependency is unreachable or unconfigured. Retry once.

Anything else in the 500s is retried once after a second, and then reported as server_error naming the status.

The four approval codes

penv reveal under an agent goes through a person. The state of that request is always a 409, and the code says which of the four states it is in.

CodeWhat it meansWhat penv does
approval_pendingNobody has answered yet, or an open request for this key and session already exists.Exit 4, with the page and the expiry.
approval_deniedA person refused it.Exit 2. Ask them, then ask again.
approval_expiredThe ten minutes ran out.Exit 4, and tells you to run penv reveal for a fresh one.
approval_redeemedThe id was already spent.Exit 4, and tells you to run penv reveal for a fresh one.

approval_denied, approval_expired and approval_redeemed live on both sides: the server sends each one on the 409, and penv prints it back under the same name with the exit code above. approval_pending is the one that changes name, because penv turns it into approval_required and hands you the page and the expiry with it. Two codes are penv's alone. A 404 from the redeem route becomes no_approval, and approval_mismatch is raised before penv ever asks the server to spend the id.

An id that covers another key never gets spent: penv reads the approval before it redeems and refuses with its own approval_mismatch. See coding agents for the whole flow.

Your identity provider

The endpoint your directory writes to speaks RFC 7644 rather than the shape above, because that is what Okta and Entra parse. The body is a SCIM error object, status is a string, and scimType carries one of the RFC's detail keywords when there is one.

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
  "status": "409",
  "scimType": "uniqueness",
  "detail": "A user with that userName already exists."
}

The content type is application/scim+json;charset=UTF-8. A throw nobody recognized becomes a 500 whose detail is always the same sentence, "The request could not be completed.", because the alternative is handing a stack trace to whoever holds a directory token.

The AWS connect callback

The callback your CloudFormation stack posts to answers a closed set of six, and one mapper decides which. A fresh create answers 200 with the lifecycle secret, a replayed create answers 200 with an empty object, and every other success is 204 with no body.

CodeStatusWhat it means
invalid_request400The body is not the shape the callback takes.
unauthorized401Anything about the token or the External ID that did not hold.
conflict409The stack is at odds with what this workspace already has.
upstream502AWS answered something the callback cannot act on.
retry503Try again. retry-after is 5 seconds.
unexpected500Nothing else fits.

An unknown token, an expired one, one already claimed, one out of attempts, a wrong External ID and a bad lifecycle secret all answer the same 401. The endpoint tells a caller whether they got in, and nothing else, so it cannot be used to find out which of those was true.

Next: global options.