The sdist's setup.py install command appends an exported PATH="<python dir>/bin/bin:$PATH" line to ~/.bashrc or ~/.zshrc, or rewrites the user PATH with setx on Windows, without asking or documenting it; the line runs no code and points at a directory that does not exist, and the published wheel does not do it.
paramsx is a Spanish-language terminal and web tool for managing AWS Systems Manager Parameter Store parameters, published to PyPI under the author name "Mariox" with the source repository github.com/pistatxos/paramsx. Every one of its 16 releases, from 1.1.0 on December 3, 2024 to 2.5.1 on November 5, 2026, ships a setup.py whose custom install command appends a line to the user's shell startup file (~/.bashrc on Linux, ~/.zshrc on macOS) whenever the package is installed from its source distribution. On Windows it rewrites the user's PATH with setx instead. The line exports PATH="<python's directory>/bin:$PATH", meant to put the paramsx command on the user's PATH. The package does not ask first, and its README does not say it does this: the README tells the reader to add $HOME/.local/bin to their shell file by hand. Because the code adds bin to a directory that is already a bin directory, the line points at a directory that does not exist, such as /tmp/build-env-<random>/bin/bin when pip installs from the sdist. It runs no code from the package and reaches no host, and nothing else in the package touches the network outside AWS's own API or the user's local browser. The hook is an unannounced edit to a shell startup file, not malware. A plain pip install paramsx picks the published wheel, which carries no setup.py, and does not run the hook. PyPI still serves all 16 versions. 2.5.1 has been up for about a day, and the hook has been in every release for about 23 months, since December 3, 2024.
What paramsx does
When paramsx 2.5.1 is installed from its source distribution, paramsx-2.5.1.tar.gz (sha256 2416b971be25c6299be09d96a911fe52bbad8eeba9e18d6fdad2b9fd64867c1e), it does this. That happens with pip install --no-binary paramsx paramsx, when a wheel is made from that sdist, with python setup.py install, or with pip install . in a checkout of the source repository:
- Setuptools runs the package's own install command,
CustomInstallCommand, while it assembles the wheel. On Linux, that command takes the directory of the Python interpreter doing the install, adds bin to it, and checks whether the result already appears in PATH.
- It never does, because the result ends in
bin/bin. So the command appends a blank line and a line exporting PATH="<dir>/bin:$PATH" to ~/.bashrc, or creates ~/.bashrc with that line if the file is missing. Under pip the interpreter is the one in pip's temporary environment, so the line reads PATH="/tmp/build-env-<random>/bin/bin:$PATH". Pip deletes that directory once the install is done.
- On macOS it writes the same line to
~/.zshrc. On Windows it runs setx PATH "<the whole current PATH>;%APPDATA%\Python\Python<XY>\Scripts", which stores the process's entire current PATH, system entries included, as the user's PATH variable.
- It prints, in Spanish, that the directory was added to the shell file. Pip hides that message unless it is run with
-v.
- Every install from source appends one more line, because the command never checks what the file already holds.
Every new interactive bash shell for that user then runs the line. Its only effect is to put a directory that does not exist at the front of PATH. The paramsx command itself is reached the normal way, from wherever pip put its console script. If another local user could create the named directory under /tmp, programs placed there would shadow the user's commands. The random part of the name is visible to anyone who can read the user's ~/.bashrc.
Installed from the wheel, paramsx-2.5.1-py3-none-any.whl (sha256 e154224b445616021049857f6a0d82028865b9a5cad51f4e6e84c77fcd3bfa82), the package writes nothing outside its own files when it is installed. Running paramsx configure creates ~/.xsoft/paramsx_config.py from a template the package ships, as its README describes. The tool then loads that file as Python, reads and writes SSM parameters through boto3 with the user's AWS profile, and paramsx --web serves a Flask page on 127.0.0.1 and opens it in the browser.
What the code does
Version 2.5.1, sdist sha256 2416b971be25c6299be09d96a911fe52bbad8eeba9e18d6fdad2b9fd64867c1e, setup.py sha256 ad4ab3de8bc4c9346b7b70e8bb4f380193b5a65c8345673ccc12ec79fd32c933 (115 lines):
setup.py:10–16 defines CustomInstallCommand(install). Its run() calls the standard install, then self._add_scripts_to_path(). setup.py:106–108 registers it as the install command (cmdclass={"install": CustomInstallCommand}). Setuptools' bdist_wheel runs the install command to lay out the wheel, so the hook runs whenever a wheel is made from this sdist, not only for setup.py install.
setup.py:32–33 sets, for Linux and macOS, scripts_dir = os.path.join(os.path.dirname(sys.executable), "bin"). sys.executable is already <prefix>/bin/python, so this yields <prefix>/bin/bin, a directory Python never creates.
setup.py:23–31 sets, for Windows, scripts_dir to ~\AppData\Roaming\Python\Python<major><minor>\Scripts, the per-user scripts directory.
setup.py:36–37 reads PATH and continues only if scripts_dir is not a substring of it, which is always the case for the bin/bin path.
setup.py:38–40 runs os.system(f'setx PATH "{path_env};{scripts_dir}"') on Windows.
setup.py:41–43 picks ~/.zshrc on macOS (Darwin) and ~/.bashrc otherwise, and calls _add_to_shell_config.
setup.py:55–70 expands the file name (line 57), composes the line exporting PATH="{scripts_dir}:$PATH" (line 58), appends \n<line>\n if the file exists (lines 61–64), or creates it with the line (lines 65–68), and prints any error instead of failing (lines 69–70).
setup.py:45–53 is the message printed when no directory could be worked out. It tells the user to add the line by hand, the same advice the README gives.
README.md:899–943 ("Configuración del PATH") tells users on Linux and macOS to add $HOME/.local/bin to their shell file themselves, then source ~/.bashrc or ~/.zshrc. It does not mention that installing from source edits those files, and the path it gives is not the one the code writes. The 2.5.1 CHANGELOG.md says nothing about it either. The wheel's file list is the paramsx package and its dist-info, with no setup.py and no .pth file, so nothing in it runs at install time.
The rest of the package, paramsx/main.py, paramsx/funcions.py, paramsx/core/ and paramsx/web/, is the SSM tool its README describes. The only file outside its own directories that it writes is the config file paramsx/main.py:668–692 creates under ~/.xsoft/, which paramsx/main.py:30–37 loads as Python, the user's own settings. It has one weakness of its own, separate from the install hook.
Parameter values from SSM are run as Python. export_parameters_to_file (paramsx/funcions.py:319–352) writes the parameters it reads from AWS SSM into a Python file, one dictionary per parameter. Each value goes inside a triple-quoted string, 'parameter_value': """{parameter_value}""" (lines 344 and 347), and each name inside single quotes, 'parameter_name': '{parameter_name}' (line 329), with no escaping. Descriptions and tags are escaped (_escapar_valor_tag, lines 303–310); values and names are not. compare_parameters (lines 408–420) and load_parameters (line 997) then read those files back with exec(f.read(), scope), and paramsx/main.py:412 calls the comparison when the user reviews changes before uploading them. A parameter whose value contains """, or whose name contains ', ends the string, and whatever follows it is run as Python, with the user's rights and AWS credentials, on the machine of whoever downloads that prefix with paramsx and then compares or uploads it. So anyone who can write a parameter under a prefix someone manages with paramsx (another team, a CI role, an account with ssm:PutParameter on that path) can run code on that person's machine. The same exec loading is in every version, 1.1.0 through 2.5.1. The 2.5.x configuration loader does not have this flaw: paramsx/core/config.py:241–271 reads the config with ast.literal_eval. paramsx/web/app.py:26 and :196 limit the web page to 127.0.0.1 and localhost. The package has no obfuscated code, no network calls beyond boto3's AWS API, and no reading of credentials other than boto3's own use of the named AWS profile.
All 16 versions carry the same hook. Version 1.1.0's setup.py (sha256 d4ba38025893…, 87 lines) has it at lines 8–59: scripts_dir at line 31, setx at line 37, the shell-file choice at line 40, the exported line at line 47, and the append or create at lines 50–57. The install class in 1.1.1 is identical to 1.1.0's. From 1.1.3 on, the class is identical to 2.5.1's, line for line, with the manual-instructions message added. Only the setup() call after it changes between releases. 2.2.1 through 2.3.1 share one setup.py, as do 2.4.1 through 2.5.1.
Timeline (UTC)
| When | What |
| 12/03/24 07:58:53 | paramsx 1.1.0 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 12/03/24 08:34:43 | paramsx 1.1.1 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 12/04/24 07:49:51 | paramsx 1.1.3 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 09/21/25 10:01:58 | paramsx 1.1.4 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 09/30/25 21:33:01 | paramsx 1.1.5 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 07/31/26 07:14:16 | paramsx 2.0.0 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 07/31/26 08:05:12 | paramsx 2.1.0 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 08/19/26 06:01:52 | paramsx 2.2.0 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 08/19/26 06:26:40 | paramsx 2.2.1 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 08/19/26 07:05:18 | paramsx 2.2.2 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 08/19/26 08:08:32 | paramsx 2.3.0 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 08/19/26 09:51:50 | paramsx 2.3.1 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 10/08/26 05:40:07 | paramsx 2.4.0 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 10/08/26 06:07:39 | paramsx 2.4.1 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 11/04/26 13:51:18 | paramsx 2.5.0 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 11/05/26 11:58:17 | paramsx 2.5.1 is published to PyPI (wheel and sdist); its setup.py carries the same shell-file hook |
| 11/06/26 | PyPI still serves all 16 versions, none yanked. 2.5.1 has been up for about a day, and the shell-file hook has shipped in every release for about 23 months, since 12/03/24 |
If you installed it
If you installed paramsx with a plain pip install paramsx, pip used the wheel and the hook did not run. If you installed it from the sdist, from a checkout of its repository, or with setup.py install, on any version:
- On Linux, look for the line in
~/.bashrc (on macOS, ~/.zshrc): grep -n 'bin/bin:\$PATH' ~/.bashrc ~/.zshrc. Delete each PATH="…/bin/bin:$PATH" line it shows; there is one per install from source. If paramsx created ~/.bashrc or ~/.zshrc, the file holds only those lines.
- On Windows, open the user environment variables and check that
Path holds what you expect. setx copied the whole PATH into it, and setx cuts values longer than 1024 characters.
- The line runs no program and sends nothing anywhere, so no credential needs rotating on account of it. paramsx itself works with your AWS profile as its README describes.
- To keep using paramsx without the hook, install it from the wheel (the default) or with
pip install --only-binary paramsx paramsx.
- Separately from the hook, do not compare or upload, with any version of paramsx, parameter files downloaded from an SSM prefix that someone else can write to: a crafted parameter value runs as Python on your machine. Read the downloaded file before paramsx loads it, or limit
ssm:PutParameter on the prefixes you manage with it.