A command works when you log in to a Linux host and type its name, but `ssh host mytool` and the same command from a crontab both fail with "command not found". Explain this in terms of which bash startup files each of those runs actually reads.
answer
- classify the shell before blaming PATH
- no pty means not interactive
- sshd runs shell -c for a command
- only BASH_ENV survives that mode
- cron never logs anyone in
basics
~20 sThe PATH entry lives in a startup file those invocations never read. ssh host mytool runs a non-interactive, non-login shell, and cron runs the command with a minimal environment, so neither ~/.bash_profile nor (in practice) ~/.bashrc applies.
solid answer
~50 sTyping the command interactively works because that session is a login and/or interactive shell, so it read `~/.bash_profile` or `~/.bashrc` and picked up the extra `PATH` entry. `ssh host mytool` is different: sshd runs your login shell with `-c` and no terminal, so bash is neither a login shell nor interactive. In that mode bash reads none of the profile or rc files; it only sources the file named by `BASH_ENV`, if that variable is already in the environment. Some builds do source `~/.bashrc` for a shell started by sshd, but the stock distro `~/.bashrc` opens with a guard that returns immediately when the shell is not interactive, so the effect is the same. Cron is stricter still — it runs the command with a short built-in `PATH` and no login at all. The fixes are an absolute path, an explicit `PATH` in the crontab or wrapper, or shipping the setting system-wide and invoking a login shell deliberately (`ssh host 'bash -lc mytool'`).
code
bash · 3 lines# Compare the two channels from your workstation
ssh host 'echo "PATH=$PATH"; echo "flags=$-"; command -v mytool || echo missing'
ssh host 'bash -lc "command -v mytool"'go deeper
Recognise that "command not found" usually means PATH, not a missing program, and that automated callers get a much smaller PATH than your terminal. Know to try the absolute path first.
Explain that ssh host cmd runs a non-interactive, non-login shell that reads no profile or rc file, and name BASH_ENV as its only hook. Say why cron's environment is smaller still.
Demonstrate the diagnosis rather than the guess: print $- and $PATH through the failing channel, compare with the interactive session, then choose the fix by blast radius — absolute path or explicit PATH for automation, system-wide profile fragment for humans.
Frame it as a contract: automated entry points must not depend on personal dotfiles. Decide where machine-level environment belongs — packaging, /etc/profile.d, unit definitions, config management — and how you stop teams reintroducing dotfile dependencies in scheduled jobs.
## The three shells involved The same binary, the same user, three different shell classifications: | How you ran it | Login? | Interactive? | Files bash reads | |---|---|---|---| | `ssh host` then type `mytool` | yes | yes | `/etc/profile`, first profile file, plus `~/.bashrc` if sourced from there | | `ssh host mytool` | no | no | none, except `$BASH_ENV` | | crontab entry | no | no | none; and cron supplies its own tiny environment | The failure is entirely explained by rows two and three reading nothing. ## What sshd actually does with a remote command With no command, sshd starts your login shell with an `argv[0]` beginning with a hyphen, which is bash's signal to behave as a login shell, and attaches a pty — so it is interactive too. With a command argument, sshd instead runs `your-shell -c 'mytool'` with no pty. Bash then has no `-l`, no leading dash and no terminal, so it is a plain non-interactive shell. Its only startup hook is `BASH_ENV`: bash expands that variable and, if the result names a readable file, sources it before running the command. `BASH_ENV` must already be in the environment, which over SSH means it has to arrive via `PermitUserEnvironment`/`~/.ssh/environment` or `SendEnv`/`AcceptEnv`, none of which is enabled by default. One complication worth naming: bash has a build-time option (used by Debian-family packages) that makes it read `~/.bashrc` when it detects it was started non-interactively by a remote shell daemon. That would rescue you, except that the same distributions ship a `~/.bashrc` beginning with ```bash case $- in *i*) ;; *) return;; esac ``` which bails out for exactly this case. So in practice nothing in your dotfiles runs. ## Why cron is even smaller Cron does not log you in at all. It runs each entry with a minimal environment: typically `PATH=/usr/bin:/bin`, plus `HOME`, `LOGNAME` and `SHELL`. Unless the crontab sets `SHELL=/bin/bash`, the command may be executed by `/bin/sh` — a different shell with its own rules (the analogous hook there is `ENV`, which bash reads only when invoked as `sh` or in POSIX mode). Adding a line to `~/.bashrc` will never influence a cron job. ## Confirming the diagnosis in 30 seconds ```bash ssh host 'echo "$PATH"; echo "flags=$-"' ``` A `PATH` shorter than your interactive one, and flags without `i`, prove the shell classification. For cron, add a temporary entry `* * * * * env > /tmp/cronenv` and compare. ## The fixes, from most to least robust 1. **Absolute path.** `/opt/tools/bin/mytool` in the crontab or the SSH command. No environment dependency at all; the right answer for automation. 2. **Set `PATH` where the invocation can see it.** Crontabs accept assignment lines: `PATH=/usr/local/bin:/usr/bin:/bin` above the entries. For SSH, pass it in the command itself. 3. **Ask for a login shell explicitly.** `ssh host 'bash -lc mytool'` forces the profile chain to run. Convenient, but slower and it inherits every side effect in the profile, so it is a poor choice inside tight automation loops. 4. **Put the setting somewhere system-wide.** A fragment in `/etc/profile.d/mytool.sh` fixes login shells for all users, and installing or symlinking the binary into a directory already on the default `PATH` (such as `/usr/local/bin`) fixes even the non-login cases. ## The general lesson Environment configured in an interactive dotfile is an *interactive convenience*, not a property of the machine. Anything an automated caller depends on must live in the invocation itself, in the crontab or unit definition, or in system-wide configuration. This is also why moving `PATH` exports out of `~/.bashrc` into a profile file does not help SSH remote commands: they read neither.
- Why does `ssh host 'bash -lc mytool'` succeed where `ssh host mytool` fails?The `-l` flag forces bash to behave as a login shell, so it reads `/etc/profile` and the user's profile file — including the `PATH` change — before running the command. It costs a full profile execution per invocation and drags in every side effect those files have, so it is fine for a one-off but a poor default inside scripts or tight loops.
- What is BASH_ENV, and why is it rarely the right fix here?For a non-interactive bash, `BASH_ENV` names a file to source before running the command. It only helps if the variable is already in the environment, which over SSH requires `SendEnv`/`AcceptEnv` or `PermitUserEnvironment` configuration, and it makes every non-interactive bash pay the cost of that file. An absolute path or an explicit `PATH` is simpler and more predictable.
- How can `sudo` produce the same symptom on a host where the command works for your own user?`sudo` sanitises the environment: with `env_reset` (the default) and `secure_path` set in sudoers, the command runs with a fixed `PATH` chosen by policy, not yours. `sudo -i` instead starts a login shell for the target user and reads that user's profile, which is why the two forms can find different binaries.
- A crontab entry uses bash-only syntax and fails with a syntax error, though the same line works in your terminal. Why?Cron executes entries with `/bin/sh` unless the crontab sets `SHELL=/bin/bash`. On Debian-family systems `/bin/sh` is dash, which does not implement bash-only constructs. Set `SHELL` explicitly in the crontab, or point the entry at a script carrying a proper bash shebang.
saying these in an interview costs you the question
- Claims ssh always runs a login shell, even with a command argument
- Says adding the export to ~/.bashrc fixes cron
- Assumes cron inherits the environment of your interactive session
- Treats "command not found" as a missing package rather than a PATH question
- Thinks BASH_ENV is read by interactive shells too