
Editorial illustration.
“But it works on my machine” is often treated as a joke. In real projects, it is a useful clue. A command that succeeds in your terminal may be running with your user account, your current directory, your full PATH, your shell aliases, your environment variables, and access to files that another process cannot see.
When the same command runs from a scheduler, deployment runner, container, IDE, or service, those assumptions change. The program may be correct; the environment around it is different. The reliable fix is to make those inputs explicit instead of adding another random flag until the error disappears.
Quick Answer
When a command works interactively but fails elsewhere, compare the execution contexts systematically. Record the user, working directory, shell, PATH, environment variables, runtime version, permissions, mounted filesystems, and input files. Log the command’s actual error and exit status. Then make the required context part of the script or service configuration.
Do not begin by running everything as root or copying your entire interactive environment into production. That can hide the cause and create a security problem. A small diagnostic report is safer and usually faster.
What “the same command” may not mean
Consider this command:
deploy-tool build --output dist
In your terminal, deploy-tool may be found through a user-specific directory in PATH. Your shell may load an alias or a version manager. You may already be inside /home/you/project, and a .env file may have been loaded by a tool. Your user may own the source and have permission to create dist.
In a service or automation job, the process may start in /, use /bin/sh, have a shorter PATH, run as runner, and receive no environment variables except the few configured by the platform. The project may be checked out in a temporary directory, and the output location may be read-only.
The text of the command is identical. The inputs are not.
A practical diagnostic sequence
1. Capture the exact error and exit code
Do not copy only the last line from a log. Capture the complete command output and the exit status:
set -o pipefail
your-command 2>&1 | tee /tmp/command-check.log
status=${PIPESTATUS[0]}
printf 'exit_status=%s\n' "$status"
exit "$status"
The exact syntax for pipeline status differs between shells, so use the construct supported by your script’s shell. The important principle is to preserve the original failure and return it to the caller. A wrapper that always exits successfully can make an automation dashboard look healthy while the work failed.
2. Compare the user and groups
Your account may have access that the service account does not. Record the identity in both contexts:
id
whoami
groups
If the process needs to read source files, write a cache, or create an output directory, identify those paths explicitly. Fix ownership, group membership, or the application’s directory layout rather than granting broad access to the whole filesystem.
3. Print the working directory
Relative paths are convenient for humans and fragile in automation. A script that reads config/settings.json is assuming where it starts. Print the directory and list the expected input:
pwd
ls -la
test -f config/settings.json && echo 'config found'
For a script, derive paths from the script’s own location when appropriate, or configure an explicit working directory in the service. Avoid changing directories repeatedly without checking that the target exists.
4. Inspect the executable and runtime versions
The command name may resolve to different binaries:
command -v node
node --version
command -v python
python --version
printf '%s\n' "$PATH"
Version managers often modify an interactive shell but are not loaded by non-interactive processes. A system service may use a different interpreter from your terminal. Record versions in logs and define the runtime deliberately through the project or deployment tool.
5. Check environment variables without leaking secrets
Environment variables frequently hold URLs, feature flags, credentials, and API keys. Log names and safe values, not the whole environment. A useful diagnostic can say DATABASE_URL=present and NODE_ENV=production without printing a password.
If a variable is required, fail with a clear message:
if [ -z "${PUBLIC_BASE_URL:-}" ]; then
echo 'PUBLIC_BASE_URL is required' >&2
exit 1
fi
Make the source of configuration clear: service file, CI secret store, deployment platform, or a checked-in example file. Never solve a missing secret by committing the real value into the repository.
6. Check the shell and quoting rules
bash, sh, zsh, PowerShell, and a CI runner do not interpret every expression the same way. A script that relies on Bash arrays should declare Bash and be run by Bash. A path containing spaces should be quoted. User input should not be concatenated into a shell command without careful handling.
At the top of a shell script, use an intentional interpreter declaration such as #!/usr/bin/env bash if that matches the deployment environment. Keep the script’s assumptions visible and test it with the same interpreter used in production.
7. Check permissions and mounts
A process may find the file but fail to read it, or it may read the source and fail to write the output. Inspect the complete path, not only the final file:
namei -l /srv/app/output/report.json
findmnt -no TARGET,OPTIONS /srv/app
A read-only mount, container volume, service sandbox, or access-control policy can block an operation even when the familiar mode bits look reasonable. Avoid “fixing” an unknown permission error by making everything world-writable.
8. Make network and external tools explicit
A terminal may have a working proxy, SSH agent, cloud login, or DNS configuration that a runner lacks. A command that downloads a dependency can fail because the job has no network access, not because the package is missing.
State whether the job requires network access, where credentials come from, and how a failure should be reported. For important deployments, prefer pinned inputs and a preflight check over discovering connectivity problems halfway through a release.
9. Use a clean environment to reproduce the failure
Once you know the successful context, remove assumptions deliberately. Run a test with a minimal PATH, an explicit working directory, and the same user where safe. This is more informative than adding sudo because it reveals which dependency was accidental.
Do this in a disposable project or staging environment when the command changes files. A diagnostic should not become an unreviewed production operation.
10. Turn the discovery into documentation or configuration
The final fix is not “remember to run this from that folder”. Put the requirement where the next execution will find it: a service unit, a CI configuration, a wrapper script, a README, a runtime file, or a preflight check.
Document the minimum user, runtime, variables, network access, input files, output directory, and expected exit status. If a new team member cannot run the command from a clean checkout by following the instructions, the command is still carrying hidden context.
A small wrapper can remove ambiguity
A wrapper script can establish its working directory, validate inputs, print safe diagnostics, and preserve failures:
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
cd "$script_dir"
: "${PUBLIC_BASE_URL:?PUBLIC_BASE_URL is required}"
test -f package.json || { echo 'package.json is missing' >&2; exit 1; }
exec npm run build
The details depend on the project, but the pattern is valuable: define the location, validate required inputs, stop on unexpected failures, and replace the wrapper process so the exit status remains meaningful.
Common mistakes to avoid
- Running the failing job as root and assuming the original account was the issue.
- Copying a whole
.envfile into logs to diagnose one missing variable. - Relying on aliases, shell startup files, or a personal version manager.
- Using relative paths without defining the working directory.
- Swallowing errors with
|| trueor an unconditional successful exit. - Fixing a missing executable by installing an unpinned version during production runs.
- Treating a permissions problem and a read-only mount as the same thing.
Why this matters beyond one script
Reproducible commands are easier to review, secure, schedule, migrate, and recover. Explicit inputs also make failures more honest. When a job says “the required directory is missing” instead of “completed”, people can act on it.
This discipline helps small websites and personal projects as much as large teams. A short, documented preflight check often saves more time than another debugging session after a deployment window has already started.
My Take
The environment is part of the program. If the command only works because your terminal quietly supplies ten assumptions, the command is not yet ready for automation. Making those assumptions visible may feel slower at first, but it creates a smaller and more trustworthy system.
Bottom Line
When a command works in your terminal but fails elsewhere, compare context before changing code. Check the user, directory, shell, runtime, variables, permissions, mounts, and network. Then encode the required context in a script or service configuration, with safe diagnostics and a meaningful exit status. Reliability starts when the command no longer depends on your memory.