# SSH startup files: remote commands agents को गलत result क्यों बताते हैं

SSH command अपने-आप वही program नहीं होता जिसे आपने टाइप किया है। Remote machine के `git`, `python` या deployment script चलाने से पहले SSH daemon एक account चुनता है, उस account का shell शुरू करता है और shell को एक command string देता है। Startup files और server rules environment बदल सकते हैं, command को बदल सकते हैं या एक invocation को interactive बना सकते हैं, जबकि दूसरा noninteractive रहता है।

जब कोई agent result रिपोर्ट करता है, तब यह और महत्वपूर्ण हो जाता है। अनपेक्षित banner, alias या रंगीन prompt देखकर इंसान रुककर जांच कर सकता है। Agent exit code 0 और परिचित दिखने वाली output line को इस बात का प्रमाण मान सकता है कि इच्छित program चला था। मैंने देखा है कि इस धारणा ने एक साधारण status check को wrapper script की call और deployment check को गलत executable के result में बदल दिया।

समाधान हर profile file मिटा देना नहीं है। लोगों को उपयोगी interactive shells चाहिए। असली समाधान है execution path पहचानना, इंसानी सुविधा को automation से अलग रखना और remote command contract को स्पष्ट बनाना।

## Remote command पहले account shell से होकर गुजरता है

OpenSSH आम तौर पर `ssh host` के बाद लिखे शब्दों को target binary के direct `execve` के रूप में नहीं चलाता। `sshd` manual के अनुसार authentication के बाद sshd requested command को user के shell के ज़रिए shell के `-c` option के साथ चलाता है। Login shell का path उस account database से आता है, जिस machine पर remote account है, न कि उस local machine से जिसने SSH connection खोला।

यह जानकारी कई उलझे हुए reports को समझाती है। मान लें agent यह भेजता है:

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

Remote account Bash, zsh, fish, restricted shell या किसी site wrapper का उपयोग कर सकता है। चुना गया shell `git -C /srv/app rev-parse HEAD` के बराबर command प्राप्त करता है। वह पहले खुद को initialize कर सकता है। इससे पहले sshd उसे replacement command भी दे सकता है।

यह उस shell से अलग है जिसे agent ने local machine पर इस्तेमाल किया। Agent Mac पर clean process से चल सकता है, जबकि remote `deploy` account में दस साल के personal shell customizations हो सकते हैं। Local prompt का clean होना remote side को clean नहीं बनाता।

Shell के command parsing को SSH transport से भी न मिलाएं। SSH connection को encrypt करता है और account को authenticate करता है। वह यह guarantee नहीं देता कि `python` वही binary है जिसकी आपको उम्मीद थी, PATH बदला नहीं है या profile ने standard output में text नहीं छापा।

इसलिए पहला व्यावहारिक सवाल «क्या SSH connect हुआ?» नहीं, बल्कि «किस program ने remote command को किस account और किन initialization rules के तहत interpret किया?» है।

## Shell mode तय करता है कि किन files का असर होगा

Shell startup behavior shell family और invocation mode पर निर्भर करता है। लोग आम तौर पर «SSH shell» या «Bash shell» कहते हैं, लेकिन ये शब्द behavior का अनुमान लगाने के लिए पर्याप्त सटीक नहीं हैं।

Bash के लिए GNU Bash Reference Manual login, interactive और noninteractive invocation में अंतर बताता है। Login Bash shell `/etc/profile` पढ़ता है, फिर `~/.bash_profile`, `~/.bash_login` और `~/.profile` में से पहली readable personal file पढ़ता है। जो interactive shell login shell नहीं है, वह `~/.bashrc` पढ़ता है।

सामान्य remote command आम तौर पर noninteractive shell में चलती है। इसका अर्थ यह नहीं कि वह कुछ भी load नहीं करती। अगर `BASH_ENV` environment variable set हो, तो Bash noninteractive command चलाने से पहले उस file को पढ़ता है। Bash manual उस special handling को भी दर्ज करता है जिसमें Bash पहचानता है कि sshd ने उसे network connection से जुड़े standard input के साथ शुरू किया है: ऐसी स्थिति में वह `~/.bashrc` पढ़ सकता है। Automation को इस लोक-धारणा पर आधारित न करें कि `.bashrc` केवल interactive use को प्रभावित करता है।

zsh का map अलग है। `/etc/zshenv` और `~/.zshenv` हर zsh invocation पर लागू होते हैं, इसलिए noisy या stateful `.zshenv` खास तौर पर खतरनाक है। zsh login shells के लिए `.zprofile` और interactive shells के लिए `.zshrc` पढ़ता है। जो user `.zshenv` में PATH edits और status output रखता है, वह remote commands, scripts और अक्सर graphical application launches को भी बदल देता है।

POSIX `sh` कोई universal escape hatch नहीं देता। Implementations अलग-अलग होती हैं। POSIX shell specification interactive shells के लिए `ENV` file बताती है, लेकिन किसी system का `/bin/sh` dash, compatibility mode में Bash या अपने अलग details वाला कोई दूसरा implementation हो सकता है। Filename हर जगह एक जैसा अर्थ रखता है, ऐसा मानने के बजाय actual host को test करें।

इस अंतर का सीधा operational असर है। Interactive troubleshooting session में सफल command agent के लिए fail हो सकती है, क्योंकि interactive session ऐसी profile पढ़ती है जिसे command session skip करता है। उलटा भी होता है: automation hook command sessions को प्रभावित कर सकता है, जबकि इंसान के terminal पर उसका असर दिखाई नहीं देता।

Interactive settings उन्हीं files में रखें जिन्हें केवल interactive shells पढ़ते हैं। Prompts, completion setup, terminal titles, color defaults और greetings वहीं होने चाहिए। Automation environment की जरूरतों को एक छोटी, documented file में रखें जिसे controlled launcher जानबूझकर source करे। Agent को developer की dotfiles विरासत में लेकर अपना runtime खोजने न दें।

## PATH बदलने से केवल सुविधा नहीं, program की पहचान बदलती है

PATH को अक्सर सुविधा की setting माना जाता है। Unattended execution में यह program की identity चुनता है। अगर profile `/opt/team/bin` को सबसे आगे जोड़ती है, तो `curl`, `git`, `ssh` या `python` system program के बजाय wrapper हो सकते हैं।

यह wrapper जानबूझकर बनाया गया हो सकता है। Teams cloud credentials चुनने, repository checks लागू करने, telemetry जोड़ने या language runtimes चुनने के लिए wrappers इस्तेमाल करती हैं। गलती यह है कि agent यह मान ले कि बिना path वाला command vendor executable है। इसका अर्थ केवल इतना है: «इस shell के मौजूदा PATH में मिला पहला matching executable।»

Aliases और functions भी ऐसी ही अस्पष्टता पैदा करते हैं, हालांकि उनका behavior shell mode पर निर्भर करता है। Bash आम तौर पर noninteractive shell में aliases expand नहीं करता, जब तक `expand_aliases` enabled न हो। इसलिए standard SSH command runs में aliases कम आम हैं, असंभव नहीं। कोई sourced file expansion enable कर सकती है। Functions को alias expansion की जरूरत नहीं होती। Profile `git` नाम का function define कर सकती है, environment state export कर सकती है और बाद की हर command को उस function से चला सकती है।

पहले shell से पूछें, लेकिन उसके answer को उस environment का evidence समझें जिसकी आप जांच कर रहे हैं, universal proof नहीं:

```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'
```

Typical result कुछ ऐसा हो सकता है:

```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
```

`command -V` builtin alias, function, builtin या pathname की जानकारी दे सकता है। जिन shells में सुविधा हो, वहां `type -a git` एक से अधिक matching paths दिखा सकता है। Bash में `declare -f git` तब function body दिखाता है जब `git` किसी function का नाम हो। इन checks से एक सामान्य failure सामने आता है: profile `git()` function define करती है, जो असली Git command चलाकर चुपचाप status event भेजता है। Output फिर भी Git output जैसी दिखती है। Side effect कहीं और होता है।

Command hashing एक और पेच है। कुछ shells command का location cache करते हैं। अगर shell ने किसी नाम को resolve करने के बाद PATH बदल दिया, तो `hash -r` या उसके equivalent से cache साफ होने तक shell cached location इस्तेमाल कर सकता है। यह आम तौर पर लंबे समय तक चलने वाले interactive shells में दिखता है, लेकिन shell process को जीवित रखने वाले agents को भी इसका सामना हो सकता है।

जिन commands की identity किसी security decision या deployment result को प्रभावित करती है, उनके लिए absolute paths इस्तेमाल करें। केवल इसलिए `/usr/bin/git` न लिखें कि वह आपके laptop पर मौजूद है। Target operating system पर expected path verify करें, उसे record करें और उस account के तहत test करें जो काम चलाएगा। अगर deployment जानबूझकर version manager या wrapper का उपयोग करता है, तो उस wrapper का नाम स्पष्ट रूप से दें और उसके contract को job का हिस्सा बनाएं।

## Pseudo-terminal formatting से कहीं अधिक बदलता है

`ssh host command` आम तौर पर pseudo-terminal allocate नहीं करता। `ssh -t host command` इसे force करता है। यह एक option shell startup branch बदल सकता है, buffering को प्रभावित कर सकता है, programs को color codes भेजने पर मजबूर कर सकता है और ऐसे input prompt करा सकता है जो non-TTY run में कभी नहीं आते।

कई shell configuration files इस तरह के test से शुरू होती हैं:

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

Shell को interactive मानने तक यह check file के बाकी हिस्से से बाहर निकल जाता है। Pseudo-terminal ऐसे हालात में shell को interactive बना सकता है जहां साधारण command connection interactive नहीं था। अब command को aliases, completion setup, prompt helpers, PATH edits और terminal-specific exports मिल सकते हैं, जो agent run में मौजूद नहीं थे।

Programs भी terminal पर सीधे प्रतिक्रिया देते हैं। कोई command progress display दिखा सकती है, output paginate कर सकती है, अलग diagnostic format चुन सकती है या confirmation input पढ़ सकती है। Machine parser, जिसे एक JSON object की उम्मीद है, fail हो सकता है क्योंकि profile program शुरू होने से पहले greeting छापती है या program TTY पहचानकर control characters भेजता है।

Mismatch की जांच करते समय दोनों modes test करें:

```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'
```

पहली command SSH से terminal allocate न करने को कहती है। दूसरी local standard input terminal न होने पर भी terminal force करती है। Output की तुलना करें, फिर दोनों modes में command resolution और PATH की तुलना करें। अगर results अलग हैं, तो पहले parser patch न करें। उस startup branch को खोजें जिसने यह अंतर पैदा किया।

Automation में default `-T` होना चाहिए। Terminal केवल उस command के लिए allocate करें जिसे सच में इसकी जरूरत हो, जैसे controlled interactive recovery task। अगर agent को routine status query चलाने के लिए terminal चाहिए, तो वह पहले ही कम predictable environment में चल रहा है।

## Server rules requested command को बदल सकती हैं

Startup files केवल एक layer हैं। Account shell तक पहुंचने से पहले server command को बदल या सीमित कर सकता है। `.bashrc` देखकर जांच रोक दी, तो वह rule छूट सकता है जिसने असल में result बदला।

`sshd_config` manual `ForceCommand` को document करता है। Administrator इसे globally या user, group, address अथवा किसी दूसरी condition के लिए `Match` block में लागू कर सकता है। उदाहरण के लिए `ForceCommand internal-sftp` के साथ server सामान्य shell commands को अनदेखा कर internal SFTP service चलाता है। Custom wrapper के साथ wrapper को requested command context मिलता है और वही तय करता है कि क्या करना है।

SSH public-key entry में account की `authorized_keys` file में `command="..."` restriction भी हो सकती है। Backup accounts, repository access, file transfer और सीमित automation में यह आम है। यह अच्छी security practice हो सकती है। समस्या तब होती है जब agent को restricted credential देकर arbitrary command execution की उम्मीद की जाती है।

Environment controls की भी समीक्षा करें। `AcceptEnv` बताता है कि sshd client से भेजे गए कौन से variables स्वीकार करेगा। `SetEnv` server-side variables set कर सकता है। `PermitUserEnvironment` enabled होने पर account की SSH environment file या authorized key options values set कर सकती हैं। ये settings PATH, locale, language runtime behavior और `BASH_ENV` जैसे hooks को प्रभावित कर सकती हैं।

Client पर `ssh -G` इस्तेमाल करें, लेकिन उसकी सीमा समझें:

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

OpenSSH आपके local `Host` blocks और defaults लागू करने के बाद client configuration दिखाता है। Output से अनपेक्षित `RemoteCommand`, forced terminal setting, environment forwarding rule या अलग target host का पता चल सकता है। यह server-side `ForceCommand`, remote account का login shell या `authorized_keys` में stored restrictions नहीं दिखा सकता।

जब account automation के लिए बना हो, तो server owner से relevant `sshd_config` और account restrictions मांगें। अगर host का administration आपके पास नहीं है, तो personal shell behavior को trial runs से reverse engineer करने के बजाय documented execution interface मांगें। Opaque forced wrapper वाला account general SSH endpoint नहीं है, भले ही वह authentication स्वीकार करता हो।

## एक ही probe पर भरोसा किए बिना execution path देखें

Diagnostic command उसी environment में चलती है जिस पर संदेह है। इससे diagnosis असंभव नहीं होता। इसका अर्थ है कि आपको कई independent facts इकट्ठे करने चाहिए और लिखना चाहिए कि हर fact क्या साबित करता है।

पहले `ssh -G` से local client configuration देखें। फिर host के account database से remote account का configured shell पता करें। कई Linux hosts पर `getent passwd deploy` colon-separated record लौटाता है, जिसका अंतिम field shell path होता है। macOS पर administrator Directory Service tools से account देख सकता है। जहां संभव हो, यह काम account के potentially customized shell के बजाय trusted administrative channel से करें।

इसके बाद actual shell की startup files प्राप्त करके review करें: global files, personal files और वे सभी files जिन्हें वे source करती हैं। इन तरह के behavior खोजें:

- `PATH=` assignments, version manager initialization और `hash` commands
- `alias`, function definitions और `expand_aliases` जैसे shell options
- `echo`, `printf` और terminal control helpers जैसे output commands
- `BASH_ENV`, `ENV`, locale variables और language-specific environment hooks
- `-t`, `$-`, `$SSH_TTY` या `$TERM` की जांच करने वाली conditional branches

सिर्फ `alias` शब्द खोजकर न रुकें। `. ~/.local/share/tool/init.sh` जैसी line उस file को source कर सकती है जिसमें आपकी जरूरत वाला function define है। Source chain को उसके अंत तक follow करें। Profile files अक्सर conditional includes का ढेर बन जाती हैं और automation behavior वहीं unreviewable होने लगता है।

फिर harmless command के साथ narrow fingerprint run करें। Shell flags, current directory, PATH, relevant environment names और job द्वारा चलाए जाने वाले exact tools का resolution record करें। हर environment variable को logs में dump न करें। Tokens और cloud credentials अक्सर वहीं होते हैं। Diagnostic transcript को execution conditions साबित करनी चाहिए, नया secret store नहीं बनना चाहिए।

अंत में exact production command को उसी mode में test करें जिसमें agent उसे चलाएगा: वही account, जरूरत न हो तो terminal के बिना, वही input transport और वही working directory। Administrator के interactive login से मिला result इसका विकल्प नहीं है। वह अलग सवाल का जवाब देता है।

Evidence को deployment या automation definition के साथ रखें। उपयोगी artifact working terminal का screenshot नहीं है। यह छोटा document है जिसमें account shell, run को प्रभावित करने वाली startup files, आवश्यक executable paths, expected environment, terminal policy और server-side command restrictions का नाम हो।

## Automation shell को जानबूझकर साधारण रखें

Reliable remote action को known shell में short environment के साथ प्रवेश करना चाहिए और फिर named binaries चलानी चाहिए। इससे sshd द्वारा इस्तेमाल किए जाने वाले initial account shell को हटाया नहीं जाता, लेकिन काम करने वाले program तक पहुंचने वाली inherited state सीमित हो जाती है।

छोटी fixed script के लिए script को standard input पर भेजें और known shell को empty environment के साथ चलाएं। Remote command string fixed रहती है, जिससे local और remote quote escaping की नाज़ुक परतें भी नहीं बनतीं।

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

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

इसके कई लाभ हैं। `-T` terminal से बचाता है। `/usr/bin/env -i` inherited variables साफ करता है। `/bin/sh -s` standard input से literal script पढ़ता है और quoted heredoc local shell को transmission से पहले `$PATH` या दूसरे text को expand करने से रोकता है। `set -eu` unset parameter या failed simple command पर रुकता है, सामान्य shell semantics के अधीन।

इसकी सीमाएं भी हैं। `/usr/bin/env`, `/bin/sh` और `/usr/bin/git` उदाहरण हैं, हर host पर इन्हें बिना जांच के copy न करें। हर target class पर paths verify करें। Environment साफ करने से वे variables हट सकते हैं जिनकी program को सच में जरूरत है, जैसे home directory, locale, proxy configuration या runtime location। केवल आवश्यक named variables वापस जोड़ें और हर variable का कारण document करें।

Broken account configuration छिपाने के लिए `env -i` का उपयोग न करें। अगर startup file initial shell को इतना बदल देती है कि fixed launcher चल ही नहीं सकता, तो account unattended work के लिए उपयुक्त नहीं है। Controlled login shell और minimal startup files वाला dedicated account इस्तेमाल करें।

Arbitrary agent text को remote `sh -c` string में डालने से बचें। Quoting mistakes intended script तक पहुंचने से पहले data को shell syntax में बदल सकती हैं। Data को constrained channel से भेजें, validated arguments वाली fixed remote script इस्तेमाल करें या structured requests के लिए बने protocol का उपयोग करें। Shell शक्तिशाली इसलिए है क्योंकि वह text को code की तरह parse करता है। Agent को गलती से इस सीमा को धुंधला करने का मौका नहीं मिलना चाहिए।

## Interactive और agent accounts अलग रखें

सबसे साफ design में इंसानों और automation के लिए अलग remote identities होती हैं। Developer account अपना prompt, completion, language managers और personal helpers रख सकता है। Automation account का declared shell, छोटा home directory और startup files होनी चाहिए जो command sessions में कुछ न करें या narrowly reviewed setup चलाएं।

यह bureaucracy नहीं है। इससे कुछ साधारण लेकिन जरूरी सवालों के जवाब मिलते हैं: कौन सा Git deploy करता है? Starting directory कौन सी है? क्या account terminal स्वीकार करता है? Release को कौन से environment names प्रभावित कर सकते हैं? Commands चलाने वाले wrapper को कौन बदल सकता है?

Dedicated account command restrictions को भी समझने योग्य बनाता है। आप उस account के लिए few permitted actions पर forced wrapper लगा सकते हैं और बाकी सब reject कर सकते हैं। इससे wrapper policy language नहीं बन जाता। उसे इतना सरल रखें कि उसकी जांच हो सके: arguments validate करे, documented environment set करे, absolute program path चलाए और audit record लिखे।

`.bashrc` में दर्जनों exceptions वाला बड़ा `case` statement रखकर समस्या हल न करें। यह तरीका लोकप्रिय है क्योंकि हर user के पास profile file पहले से होती है और deployment change की जरूरत नहीं पड़ती। लेकिन profile semantics shell mode के अनुसार बदलती हैं, source order अस्पष्ट हो जाता है और interactive edit unattended behavior बदल सकती है। Automation setup को उस launcher या dedicated script में रखें जो automation contract का मालिक है।

Sallyport SSH credentials को agent process से दूर रख सकता है और bundled `sp-ssh` helper के ज़रिए SSH actions चला सकता है, लेकिन वह uncontrolled remote account को deterministic नहीं बना सकता। Remote account review को action design का हिस्सा मानें, फिर activity record से तुलना करें कि agent ने क्या request किया और remote side ने क्या लौटाया।

## Contract स्पष्ट करने के बाद ही results पर भरोसा करें

Agent command result तब trustworthy होता है जब आप बता सकें कि क्या चला, बिना किसी व्यक्ति की मौजूदा shell habits पर निर्भर हुए। इसके लिए hostname और exit code से अधिक जानकारी चाहिए। इसमें SSH account, target host identity, terminal setting, server command restriction, selected shell, environment construction, script source और executable path शामिल होने चाहिए।

यहां एक असुविधाजनक सीमा है। Remote command के अपने output से आप यह साबित नहीं कर सकते कि remote shell ने output छापने से पहले command को नहीं बदला। अगर account shell, authorized key restriction या forced command आपके नियंत्रण में नहीं है, तो आपको administrative evidence या अलग account चाहिए। एक ही probe को दस बार दोहराने से वही assumption दस बार दोहरती है।

सबसे पहले उन commands से शुरू करें जिन्हें agents code, infrastructure या production data बदलने के लिए चलाते हैं। हर command को terminal के साथ और उसके बिना चलाएं, देखें कि उसका executable कैसे resolve होता है और जहां result महत्वपूर्ण हो वहां inherited setup को explicit launcher से बदलें। पहला surprise आम तौर पर PATH होता है। दूसरा अक्सर वह startup file होती है जिसे कोई अब भी source किया जाना भूल चुका है।

ऐसा surprise मिलने पर profile में एक और conditional जोड़कर इसे fixed न मानें। Behavior को controlled account या fixed script में ले जाएं, command contract record करें और अगला agent run इतना साधारण बनाएं कि उसका output वही अर्थ दे जो वह कहता है।
