Skip to content
shellhacks.dev
CI/CD

Jenkins Error Script Returned Exit Code 127 With Every Cause

Fix Jenkins script returned exit code 127: command not found, agent PATH differences, missing tools, Windows line endings and wrong interpreters.

6 min read
Summarize in AI
Illustration for "Jenkins Error Script Returned Exit Code 127 With Every Cause": a terminal session.

The console ends with script returned exit code 127, and above it a line such as mvn: command not found. The status 127 is the shell's reserved code for a command that does not exist. The question is why it is missing from this agent when it works on your machine. The details are for Jenkins 2.462 with declarative pipelines on a Linux agent; the general meaning of the code is in exit codes 126, 127, 130 and 137.

See what the agent sees

Text
pipeline {
  agent { label 'linux' }
  stages {
    stage('diagnose') {
      steps {
        sh '''
          echo "node: $NODE_NAME  user: $(id -un)  shell: $SHELL"
          echo "PATH=$PATH"
          command -v mvn java git docker || true
          uname -a
        '''
      }
    }
  }
}
Text
node: build-03  user: jenkins  shell: /bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/usr/bin/java
/usr/bin/git

The output shows what is installed and where. Here mvn is missing, which is the cause. Agents run as a service account such as jenkins, with a non-interactive shell, so your .bashrc and .profile have no effect, and tools installed in a home directory or with a version manager are not on the PATH. The same mechanism affects scheduled jobs, as in scripts that work in a terminal but not in cron.

Fix 1: install the tool or declare it

Text
pipeline {
  agent any
  tools { maven 'maven-3.9'; jdk 'temurin-21' }
  stages {
    stage('build') { steps { sh 'mvn -B -ntp verify' } }
  }
}

The tools directive installs and adds a tool configured under Manage Jenkins, Tools, to the PATH for the job. This works for Maven, JDK, Gradle, Node and others with a matching plugin. For anything else, install the package on the agent image, or use a container agent so that the toolchain is part of the pipeline:

Text
pipeline {
  agent { docker { image 'maven:3.9-eclipse-temurin-21' } }
  stages { stage('build') { steps { sh 'mvn -B verify' } } }
}

A container image pins the tool version, and an agent without Docker access fails differently, see permission denied on docker.sock.

Fix 2: set PATH explicitly

Text
environment { PATH = "/opt/tools/bin:${env.PATH}" }

Set the path in the pipeline, or in the node's configuration under "Environment variables", rather than relying on the profile. Use absolute paths in scripts that run on several agents, and avoid sh -l to load a profile, which depends on the agent's setup.

Fix 3: the script itself

Shell
head -1 ci/build.sh | od -c | head -2
file ci/build.sh

A script that starts with #!/bin/bash followed by a carriage return from a Windows editor makes the kernel look for an interpreter named bash plus a carriage return, and the shell reports not found; the status can be 127. See changing line endings. Jenkins' sh step uses /bin/sh unless a shebang line says otherwise, and on Debian that is dash, which rejects Bash syntax with its own errors, so add a shebang or call bash script.sh. A missing interpreter, such as python3 on a minimal image, gives 127 too.

Why a Jenkins step has a different environment

A pipeline step does not run in your interactive shell. The agent process is started by a service manager or a container runtime, and it gives each sh step a fresh, non-interactive, non-login shell. Startup files such as .bashrc and .profile are not read, so version managers like nvm, pyenv and sdkman that initialise themselves there have not run, and their shims are not on PATH. The default shell for the sh step is /bin/sh, which on Debian is dash and has no arrays or [[ ]], so a script that assumes Bash can fail with other errors. A step that runs as the jenkins user also sees that user's home directory and permissions, not yours. Almost every exit 127 comes from one of these differences, and printing the environment settles which.

The error text identifies the missing name. "mvn: command not found" means the shell could not find mvn on PATH. "/bin/sh: 1: source: not found" is a different case: the command exists in Bash only. A message that names a script, like "./gradlew: not found" when the file exists, usually means the file's first line points at an interpreter that is not installed, or that Windows line endings made the interpreter name end in a carriage return.

Declaring tools instead of assuming them

Jenkins offers several ways to supply tools. The Global Tool Configuration lets an administrator define JDK, Maven and Node installations, and a pipeline selects them with a tools block, which puts the right directories on PATH for the stage. A container agent, using agent { docker { image 'maven:3.9-eclipse-temurin-21' } }, brings the exact toolchain with it and removes the dependency on what is installed on the machine. The second approach scales better, since the pipeline file describes its own environment and different branches can use different versions. Pin the image tag, so a new release does not change the build without a commit.

Shell options in pipelines

Set the shell explicitly with a first line of #!/usr/bin/env bash and set -euo pipefail in a multi-line sh block, or use the sh(script: ..., label: ...) form with a shebang. By default, Jenkins runs a shell with -xe, which echoes commands and stops at the first failure, so output is verbose and secrets used in expansions can appear in the log unless masked by credentials bindings. Use withCredentials for secrets, and avoid string interpolation of credentials in Groovy double quotes, which expands them in the Jenkins process before the shell and can leak them. Single-quoted strings pass the variable name through for the shell to expand.

Windows agents and other causes

On Windows agents, the bat step runs cmd.exe and the exit code for a missing command is 1 or 9009, and not 127, so a 127 on Windows points to a Git Bash or WSL shell inside a sh step. Other causes include a tool installed in a different path on different agents in a pool, so the build passes on one node and fails on another; a label that matches agents with different images; and a missing executable bit on a script checked out by a tool that does not preserve modes. Compare a failing agent with a passing one using the diagnostic stage, listing the same paths on both.

Making it stay fixed

Keep the diagnostic stage in the pipeline as a small, optional step that runs when a parameter is set, since the next unexplained failure will need it. Treat tool versions as part of the code: define them in the repository, via a container image, a tool installer block or a version file read by the build. Fail early with a clear message, by checking required commands at the start of the pipeline with command -v in a loop and printing the list of those missing. A pipeline that says "missing tools: mvn, node" in the first ten seconds saves everyone from reading a long log.

Where this does not apply

Exit 126 means the file exists but cannot be executed, a permission or noexec issue, and 1 means the command ran and failed, as in GitLab CI exit code 1. On a Windows agent the bat step has its own codes, where 9009 is "command not found". See the Jenkins pipeline syntax for tools and the sh step reference.

Also inDebugging
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.