# Archivos de inicio de SSH: por qué los comandos remotos engañan a los agentes

Un comando SSH no es automáticamente el programa que escribiste. Antes de que la máquina remota ejecute `git`, `python` o un script de despliegue, el demonio SSH selecciona una cuenta, inicia el shell de esa cuenta y le entrega una cadena de comandos. Los archivos de inicio y las reglas del servidor pueden cambiar el entorno, sustituir el comando o hacer que una ejecución sea interactiva mientras otra sigue siendo no interactiva.

Esto importa aún más cuando un agente informa del resultado. Una persona que ve un banner inesperado, un alias o un prompt en color puede detenerse e investigar. Un agente puede interpretar el código de salida 0 y una línea de salida conocida como prueba de que se ejecutó el programa previsto. He visto cómo esa suposición convertía una comprobación de estado inofensiva en una llamada a un script wrapper y una comprobación de despliegue en el resultado del ejecutable equivocado.

La solución no consiste en borrar todos los archivos de perfil. Las personas necesitan shells interactivos útiles. Hay que identificar la ruta de ejecución, separar la comodidad humana de la automatización y hacer explícito el contrato del comando remoto.

## Un comando remoto pasa primero por el shell de la cuenta

OpenSSH normalmente no ejecuta las palabras que siguen a `ssh host` mediante un `execve` directo del binario de destino. El manual de `sshd` indica que, después de la autenticación, sshd ejecuta el comando solicitado a través del shell del usuario, usando la opción `-c` del shell. La ruta del shell de inicio de sesión procede de la base de datos de cuentas, no de la máquina local que abrió la conexión SSH.

Ese detalle explica muchos informes confusos. Supón que un agente envía:

```text
ssh deploy@buildbox git -C /srv/app rev-parse HEAD
```

La cuenta remota podría usar Bash, zsh, fish, un shell restringido o un wrapper del sitio. El shell seleccionado recibe un comando equivalente a `git -C /srv/app rev-parse HEAD`. Puede inicializarse primero. También puede recibir un comando sustituido por sshd antes de llegar a ese punto.

Esto es independiente del shell que el agente usó localmente. Un agente puede ejecutarse desde un proceso limpio en un Mac mientras la cuenta remota `deploy` conserva diez años de personalizaciones del shell. Un prompt local limpio no vuelve limpio el extremo remoto.

Tampoco confundas el análisis de comandos del shell con el transporte SSH. SSH cifra la conexión y autentica la cuenta. No garantiza que `python` sea el binario que esperabas, que PATH no haya cambiado ni que un perfil no haya escrito texto en la salida estándar.

Por eso, la primera pregunta práctica no es «¿SSH se conectó?». Es «¿Qué programa interpretó el comando remoto, con qué cuenta y bajo qué reglas de inicialización?»

## El modo del shell decide qué archivos intervienen

El comportamiento de inicio del shell depende de la familia del shell y del modo de invocación. Las etiquetas que se usan informalmente, «un shell SSH» o «un shell Bash», no son lo bastante precisas para predecir el comportamiento.

En Bash, el Manual de referencia de GNU Bash distingue entre la invocación de inicio de sesión, interactiva y no interactiva. Un shell Bash de inicio de sesión lee `/etc/profile` y después el primer archivo personal legible entre `~/.bash_profile`, `~/.bash_login` y `~/.profile`. Un shell interactivo que no es de inicio de sesión lee `~/.bashrc`.

Un comando remoto normal suele ejecutarse en un shell no interactivo. Eso no significa que no cargue nada. Bash lee el archivo indicado por `BASH_ENV`, si esa variable de entorno está definida, antes de ejecutar un comando no interactivo. El manual de Bash también documenta un tratamiento especial cuando Bash detecta que sshd lo inició con la entrada estándar conectada a una conexión de red: puede leer `~/.bashrc`. No bases la automatización en la regla popular de que `.bashrc` solo afecta al uso interactivo.

zsh tiene otro mapa. `/etc/zshenv` y `~/.zshenv` se aplican a todas las invocaciones de zsh, lo que vuelve especialmente peligroso un `.zshenv` ruidoso o con efectos persistentes. zsh lee `.zprofile` para los shells de inicio de sesión y `.zshrc` para los shells interactivos. Quien coloca cambios de PATH y mensajes de estado en `.zshenv` también ha cambiado los comandos remotos, los scripts y, a menudo, el inicio de aplicaciones gráficas.

POSIX `sh` no ofrece una vía de escape universal. Las implementaciones varían. La especificación del shell POSIX describe el archivo `ENV` para los shells interactivos, pero el `/bin/sh` de un sistema puede ser dash, Bash en modo de compatibilidad u otra implementación con sus propios detalles. Prueba el host real en lugar de asumir que un nombre de archivo significa lo mismo en todas partes.

La distinción tiene una consecuencia operativa clara. Un comando que funciona en una sesión interactiva de diagnóstico puede fallar para un agente porque la sesión interactiva lee un perfil que la sesión del comando omite. También ocurre lo contrario: un hook de automatización puede afectar a las sesiones de comandos mientras la terminal de una persona no lo nota.

Mantén los ajustes interactivos en archivos que solo lean los shells interactivos. Los prompts, la configuración del autocompletado, los títulos de terminal, los valores predeterminados de color y los saludos deben ir allí. Coloca los requisitos de entorno de la automatización en un archivo pequeño y documentado que un lanzador controlado cargue de forma deliberada. No permitas que un agente descubra su entorno de ejecución heredando los dotfiles de un desarrollador.

## Los cambios en PATH alteran la identidad, no solo la comodidad

PATH suele tratarse como un ajuste de comodidad. En una ejecución desatendida, selecciona la identidad del programa. Si un perfil antepone `/opt/team/bin`, entonces `curl`, `git`, `ssh` o `python` pueden referirse a un wrapper en lugar del programa del sistema.

Ese wrapper puede ser intencionado. Los equipos usan wrappers para elegir credenciales de nube, imponer comprobaciones de repositorios, añadir telemetría o seleccionar entornos de ejecución de lenguajes. El error consiste en permitir que un agente suponga que un comando sin calificar significa el ejecutable del proveedor. Solo significa «el primer ejecutable coincidente en el PATH que este shell tiene en ese momento».

Los alias y las funciones crean una ambigüedad parecida, aunque su comportamiento depende del modo del shell. Bash normalmente no expande alias en un shell no interactivo, a menos que `expand_aliases` esté activado. Por eso los alias son menos comunes en las ejecuciones estándar de comandos SSH, pero no imposibles. Un archivo cargado puede activar la expansión. Las funciones no necesitan expansión de alias. Un perfil puede definir una función llamada `git`, exportar el estado del entorno y hacer que todos los comandos posteriores de ese shell llamen a la función.

Empieza preguntando al shell, pero considera su respuesta una prueba del entorno que estás inspeccionando, no una demostración universal:

```sh
ssh -T deploy@buildbox '\nprintf "shell argv0: %s\\n" "$0"\nprintf "shell flags: %s\\n" "$-"\nprintf "PATH: %s\\n" "$PATH"\ncommand -V git\ncommand -V python\ncommand -V curl\n'
```

Un resultado habitual podría ser este:

```text
shell argv0: -bash
shell flags: hBc
PATH: /opt/team/bin:/usr/local/bin:/usr/bin:/bin
git is /opt/team/bin/git
python is /usr/local/bin/python
curl is /usr/bin/curl
```

El builtin `command -V` puede informar de un alias, una función, un builtin o una ruta. En los shells que lo admiten, `type -a git` puede mostrar más de una ruta coincidente. En Bash, `declare -f git` imprime el cuerpo de una función si `git` es el nombre de una función. Estas comprobaciones dejan al descubierto un fallo común: un perfil define una función `git()` que ejecuta un comando Git real y después envía discretamente un evento de estado. La salida sigue pareciendo salida de Git. El efecto secundario ocurre en otro lugar.

La caché de comandos añade otra complicación. Algunos shells guardan dónde encontraron un comando. Si un perfil cambia PATH después de que el shell haya resuelto un nombre, el shell puede seguir usando la ubicación almacenada hasta que `hash -r` o su equivalente borre la caché. Esto aparece sobre todo en shells interactivos de larga duración, pero los agentes que mantienen vivo un proceso de shell también pueden encontrárselo.

Usa rutas absolutas para los comandos cuya identidad afecte a una decisión de seguridad o a un resultado de despliegue. No escribas `/usr/bin/git` solo porque exista en tu portátil. Verifica la ruta esperada en el sistema operativo de destino, regístrala y pruébala con la cuenta que ejecutará el trabajo. Si el despliegue utiliza deliberadamente un gestor de versiones o un wrapper, nombra ese wrapper de forma explícita y convierte su contrato en parte del trabajo.

## Un pseudo-terminal cambia más que el formato

`ssh host command` normalmente no asigna un pseudo-terminal. `ssh -t host command` lo fuerza. Esa sola opción puede cambiar las ramas de inicio del shell, afectar al buffering, hacer que los programas emitan códigos de color y provocar solicitudes de entrada que una ejecución sin TTY nunca haría.

Muchos archivos de configuración del shell empiezan con una comprobación como esta:

```sh
case $- in
  *i*) ;;
  *) return ;;
esac
```

Esa comprobación sale del resto del archivo a menos que el shell se identifique como interactivo. Un pseudo-terminal puede hacer que un shell sea interactivo en circunstancias en las que una conexión de comandos normal no lo sería. El comando recibe entonces alias, configuración del autocompletado, helpers del prompt, cambios de PATH y exportaciones específicas del terminal que no estaban presentes en la ejecución del agente.

Los programas también reaccionan directamente al terminal. Un comando puede mostrar una barra de progreso, paginar la salida, elegir otro formato de diagnóstico o leer una confirmación. Un parser de máquina que espera un único objeto JSON puede fallar porque un perfil imprime un saludo antes de que se inicie el programa o porque el programa detecta un TTY y emite caracteres de control.

Prueba ambos modos cuando investigues una discrepancia:

```sh
ssh -T deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'
ssh -tt deploy@buildbox 'printf "flags=%s tty=" "$-"; test -t 1 \u0026\u0026 echo yes || echo no'
```

El primer comando pide a SSH que no asigne un terminal. El segundo fuerza uno incluso cuando la entrada estándar local no es un terminal. Compara la salida y después compara la resolución de comandos y PATH en cada modo. Si los resultados difieren, no corrijas primero el parser. Encuentra la rama de inicio que produjo la diferencia.

La automatización debería usar `-T` de forma predeterminada. Asigna un terminal solo a los comandos que realmente lo necesiten, como una tarea controlada de recuperación interactiva. Si un agente necesita un terminal para ejecutar una consulta de estado rutinaria, ya está trabajando en un entorno menos predecible.

## Las reglas del servidor pueden sustituir el comando solicitado

Los archivos de inicio son solo una capa. El servidor puede sustituir o restringir un comando antes de que el shell de la cuenta lo vea. Si inspeccionas `.bashrc` y te detienes ahí, puedes pasar por alto la regla que realmente cambió el resultado.

El manual de `sshd_config` documenta `ForceCommand`. Un administrador puede aplicarlo globalmente o dentro de un bloque `Match` para un usuario, grupo, dirección u otra condición. Con `ForceCommand internal-sftp`, por ejemplo, el servidor ignora los comandos de shell normales y ejecuta el servicio SFTP interno. Con un wrapper personalizado, el wrapper recibe el contexto del comando solicitado y decide qué hacer.

Una entrada de clave pública SSH también puede incluir una restricción `command="..."` en el archivo `authorized_keys` de la cuenta. Esto es habitual en cuentas de copias de seguridad, acceso a repositorios, transferencia de archivos y automatización con un alcance limitado. Puede ser una buena práctica de seguridad. Se convierte en un problema de depuración cuando alguien entrega a un agente una credencial restringida y espera que ejecute comandos arbitrarios.

Los controles del entorno también forman parte de la revisión. `AcceptEnv` indica a sshd qué variables proporcionadas por el cliente acepta. `SetEnv` puede establecer variables en el servidor. `PermitUserEnvironment`, cuando está activado, puede permitir que el archivo de entorno SSH de una cuenta o las opciones de una clave autorizada establezcan valores. Estos ajustes pueden influir en PATH, la configuración regional, el comportamiento del runtime de un lenguaje y hooks como `BASH_ENV`.

Usa `ssh -G` en el cliente, pero conoce sus límites:

```sh
ssh -G buildbox | grep -E '^(hostname|user|port|requesttty|remotecommand|sendenv|setenv) '
```

OpenSSH muestra la configuración del cliente después de aplicar los bloques `Host` y los valores predeterminados locales. La salida puede revelar un `RemoteCommand` inesperado, una configuración forzada del terminal, una regla de reenvío de entorno o un host de destino diferente. No puede revelar `ForceCommand` del servidor, el shell de inicio de sesión de la cuenta remota ni las restricciones guardadas en `authorized_keys`.

Pide al propietario del servidor la configuración relevante de `sshd_config` y las restricciones de la cuenta cuando esta esté destinada a la automatización. Si no administras el host, solicita una interfaz de ejecución documentada en lugar de intentar reconstruir el comportamiento de un shell personal mediante pruebas sucesivas. Una cuenta con un wrapper forzado y opaco no es un endpoint SSH general, aunque acepte la autenticación.

## Inspecciona la ruta de ejecución sin confiar en una sola prueba

Un comando de diagnóstico se ejecuta dentro del mismo entorno que está bajo sospecha. Eso no hace imposible el diagnóstico. Significa que debes recopilar varios datos independientes y especificar qué demuestra cada uno.

Primero, inspecciona la configuración del cliente local con `ssh -G`. Después determina el shell configurado para la cuenta remota mediante la base de datos de cuentas del host. En muchos hosts Linux, `getent passwd deploy` devuelve un registro separado por dos puntos cuyo último campo es la ruta del shell. En macOS, un administrador puede inspeccionar la cuenta con las herramientas de Directory Service. Cuando sea posible, hazlo desde un canal administrativo de confianza y no a través del shell potencialmente personalizado de la cuenta.

A continuación, obtén y revisa los archivos de inicio del shell real: archivos globales, archivos personales y todos los archivos que estos carguen. Busca estos tipos de comportamiento:

- asignaciones de `PATH=`, inicialización de gestores de versiones y comandos `hash`
- `alias`, definiciones de funciones y opciones del shell como `expand_aliases`
- comandos que escriban en la salida, como `echo`, `printf` y helpers de control del terminal
- `BASH_ENV`, `ENV`, variables de configuración regional y hooks de entorno específicos de lenguajes
- ramas condicionales que comprueben `-t`, `$-`, `$SSH_TTY` o `$TERM`

No busques únicamente la palabra `alias`. Una línea como `. ~/.local/share/tool/init.sh` puede cargar el archivo que define la función que te interesa. Sigue la cadena de archivos cargados hasta el final. Los archivos de perfil suelen crecer hasta convertirse en una acumulación de inclusiones condicionales, y ahí es donde el comportamiento de la automatización deja de poder revisarse con claridad.

Después realiza una ejecución de huella limitada con un comando inofensivo. Registra los indicadores del shell, el directorio actual, PATH, los nombres de entorno relevantes y la resolución exacta de las herramientas que utilizará el trabajo. Evita volcar todas las variables de entorno en los registros. Los tokens y las credenciales de nube suelen vivir allí. Una transcripción de diagnóstico debe demostrar las condiciones de ejecución sin convertirse en un nuevo almacén de secretos.

Por último, prueba el comando exacto de producción en el modo exacto que usará el agente: la misma cuenta, sin terminal salvo que sea necesario, el mismo transporte de entrada y el mismo directorio de trabajo. Un resultado obtenido mediante el inicio de sesión interactivo de un administrador no lo sustituye. Responde a otra pregunta.

Conserva las pruebas junto con la definición del despliegue o de la automatización. El recurso útil no es una captura de pantalla de una terminal funcionando. Es un documento breve que indique el shell de la cuenta, los archivos de inicio que pueden afectar a la ejecución, las rutas de los ejecutables necesarios, el entorno esperado, la política de terminales y las restricciones de comandos del servidor.

## Haz que el shell de automatización sea deliberadamente aburrido

Una acción remota fiable debería entrar en un shell conocido con un entorno reducido y ejecutar después binarios identificados. Esto no elimina el shell inicial de la cuenta que usa sshd, pero limita cuánto estado heredado llega al programa que realiza el trabajo.

Para un script pequeño y fijo, envía el script por la entrada estándar y ejecuta un shell conocido con un entorno vacío. La cadena de comandos remota permanece fija, lo que también evita construir una frágil torre de escapes de comillas locales y remotas.

```sh
ssh -T deploy@buildbox '/usr/bin/env -i PATH=/usr/bin:/bin /bin/sh -s' \u003c\u003c'REMOTE'
set -eu
PATH=/usr/bin:/bin
export PATH

/usr/bin/git -C /srv/app rev-parse HEAD
REMOTE
```

Esto ofrece varias ventajas. `-T` evita un terminal. `/usr/bin/env -i` borra las variables heredadas. `/bin/sh -s` lee el script literal desde la entrada estándar y el heredoc entre comillas impide que el shell local expanda `$PATH` u otro texto antes de transmitirlo. `set -eu` detiene la ejecución ante un parámetro no definido o un comando simple fallido, sujeto a la semántica normal del shell.

También tiene límites. `/usr/bin/env`, `/bin/sh` y `/usr/bin/git` son ejemplos, no rutas que debas copiar a ciegas en todos los hosts. Confirma las rutas en cada clase de destino. Limpiar el entorno puede eliminar variables que un programa necesita legítimamente, como el directorio personal, la configuración regional, la configuración del proxy o la ubicación del runtime. Añade únicamente las variables identificadas que el programa necesite y documenta por qué existe cada una.

No uses `env -i` para ocultar una configuración defectuosa de la cuenta. Si un archivo de inicio modifica el shell inicial de forma tan agresiva que impide ejecutar el lanzador fijo, la cuenta no es adecuada para trabajos desatendidos. Usa una cuenta específica con un shell de inicio controlado y archivos de inicio mínimos.

Evita introducir texto arbitrario del agente en una cadena remota `sh -c`. Los errores de comillas pueden convertir los datos en sintaxis del shell antes de que el script previsto los reciba. Pasa los datos por un canal restringido, usa un script remoto fijo con argumentos validados o emplea un protocolo diseñado para solicitudes estructuradas. El shell es potente porque analiza el texto como código. Un agente no debería difuminar accidentalmente ese límite.

## Separa las cuentas interactivas de las cuentas de agentes

El diseño más limpio asigna identidades remotas distintas a las personas y a la automatización. Una cuenta de desarrollador puede conservar su prompt, autocompletado, gestores de lenguajes y helpers personales. Una cuenta de automatización debería tener un shell declarado, un directorio personal pequeño y archivos de inicio que no hagan nada en las sesiones de comandos o ejecuten una configuración revisada de forma estricta.

Esta separación no es burocracia. Permite responder preguntas sencillas pero necesarias: ¿Qué binario de Git hace el despliegue? ¿Cuál es el directorio inicial? ¿La cuenta acepta un terminal? ¿Qué nombres de entorno pueden afectar a una versión? ¿Quién puede cambiar el wrapper que ejecuta los comandos?

Una cuenta específica también facilita razonar sobre las restricciones de comandos. Puedes usar un wrapper forzado para las pocas acciones que esa cuenta debe realizar y rechazar todo lo demás. Eso no convierte el wrapper en un lenguaje de políticas. Mantenlo lo bastante sencillo para inspeccionarlo: valida los argumentos, establece el entorno documentado, invoca una ruta absoluta del programa y escribe un registro de auditoría.

No resuelvas esto colocando un gran `case` con docenas de excepciones en `.bashrc`. Este enfoque es popular porque cada usuario ya tiene un archivo de perfil y no hace falta cambiar el despliegue. Falla porque la semántica del perfil varía según el modo del shell, el orden de carga se vuelve oscuro y una edición interactiva inofensiva puede alterar el comportamiento desatendido. Coloca la configuración de automatización en el lanzador o en el script específico que sea responsable del contrato de automatización.

Sallyport puede mantener las credenciales SSH fuera del proceso del agente y ejecutar acciones SSH mediante su helper `sp-ssh` incluido, pero no puede hacer determinista una cuenta remota sin control. Trata la revisión de la cuenta remota como parte del diseño de la acción y usa después el registro de actividad para comparar lo que solicitó el agente con lo que devolvió el extremo remoto.

## Confía en los resultados solo después de fijar el contrato

El resultado de un comando de agente es fiable cuando puedes afirmar qué se ejecutó sin depender de los hábitos actuales del shell de una persona. Esa afirmación necesita más que un hostname y un código de salida. Debe identificar la cuenta SSH, la identidad del host de destino, la configuración del terminal, la restricción de comandos del servidor si existe, el shell seleccionado, la construcción del entorno, el origen del script y la ruta del ejecutable.

Aquí existe un límite incómodo. No puedes demostrar mediante la propia salida de un comando remoto que el shell remoto no modificó el comando antes de imprimirla. Si el shell de la cuenta, la restricción de la clave autorizada o el comando forzado están fuera de tu control, necesitas pruebas administrativas o una cuenta diferente. Repetir la misma prueba diez veces solo repite la misma suposición.

Empieza por los comandos que los agentes ya ejecutan y que pueden cambiar código, infraestructura o datos de producción. Para cada uno, ejecútalo con y sin terminal, comprueba cómo se resuelve su ejecutable y sustituye la configuración heredada por un lanzador explícito cuando ese resultado importe. La primera sorpresa suele ser PATH. La segunda suele ser un archivo de inicio que alguien olvidó que todavía se estaba cargando.

Cuando encuentres una de esas sorpresas, no añadas otra condición a un perfil y lo des por solucionado. Mueve el comportamiento a una cuenta controlada o a un script fijo, registra el contrato del comando y haz que la siguiente ejecución del agente sea lo bastante aburrida como para que su salida signifique exactamente lo que dice.
