I first wanted to call a small Python tool from a Cowork scheduled task around the time I was wiring quality gates into my four-site article pipeline. Just a quick pandas aggregation. Should have been one line.
$ pip install pandas
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
Run pip in an Ubuntu 22.04-based sandbox and you hit this. It is not about sudo, networking, or a PyPI outage. PEP 668 (Externally Managed Environments), adopted widely from Python 3.11 onward, is intentionally refusing the install to keep the system-wide Python from being broken.
I'm Masaki Hirokawa, an indie iOS/Android developer and artist who has been shipping apps since 2014 (50M+ cumulative downloads). I run daily article-publishing pipelines on four sites via Cowork scheduled tasks, and small Python utilities show up in those pipelines often. Here is the diagnosis flow and the three fix patterns I use, drawn from running into this exact error more than once.
Why PEP 668 rejects pip
Linux distributions like Ubuntu and Debian rely on Python for parts of the OS itself. apt has Python-based components; sandboxes like Cowork's also have toolchains (Node, ripgrep, others) that depend on the system Python.
If you overwrite a globally installed package with pip install, the version the OS expected silently shifts, and other tools may stop working. PEP 668 prevents that by letting the distributor drop a marker file — EXTERNALLY-MANAGED — that tells pip "do not touch this Python."
You can confirm the marker in one line.
ls /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null && echo "PEP 668 environment"In a Cowork sandbox, /usr/lib/python3.10/EXTERNALLY-MANAGED is there. The error is correct behaviour, not a bug.
Pattern 1: --break-system-packages to make your intent explicit
The smallest-effort option. Tell pip "I know this might break things, install anyway."
pip install pandas --break-system-packagesFor one-line scheduled-task work, or when you only need one or two packages for a quick aggregation, this is usually enough. Cowork's system Python is largely disposable per VM, so even if you break it, the next session rebuilds the environment.
Two caveats. First, even when pip install succeeds, it may conflict with a same-name package shipped by the distribution. Always confirm where the install landed.
pip show pandas | grep Location
# Location: /usr/local/lib/python3.10/dist-packagesIf it shows /usr/local/lib/.../dist-packages, you have not overwritten the distribution's /usr/lib/python3/dist-packages. Python's import precedence favours /usr/local, so the newer copy is loaded.
Second, do not use this flag in clean CI environments. The venv approach below is more reproducible and easier to track for dependencies.
Pattern 2: Isolate with venv (recommended)
Slightly more steps, but the most honest option if you actually use Python in scheduled tasks on a regular basis.
python3 -m venv /tmp/venv
source /tmp/venv/bin/activate
pip install pandas requests
python my_script.pyPEP 668's marker has no effect inside a venv, so neither --break-system-packages nor sudo is needed. Delete /tmp/venv when you're done and cleanup is trivial.
When using this in Cowork scheduled tasks, remember that each bash invocation is a fresh shell — env vars from one call do not survive to the next. Keep the source and the script execution inside a single bash call, or invoke /tmp/venv/bin/python by its full path.
# All in one bash invocation
python3 -m venv /tmp/venv && \
/tmp/venv/bin/pip install -q pandas requests && \
/tmp/venv/bin/python /sessions/*/mnt/work/script.pyWhen called this way from a SKILL.md scheduled task, you get a clean environment every run even across sandbox restarts. This is the shape I eventually settled on for stabilising the article-publishing pipeline.
Pattern 3: pipx for isolated CLI installs
For single CLI tools like black or ruff, pipx is the cleaner fit. It builds a venv for each tool automatically and only exposes the CLI's symlink on your PATH.
pip install pipx --break-system-packages
pipx install ruff
ruff --versionPipx itself still needs --break-system-packages to bootstrap — a one-time compromise to put pipx into the system. After that, every pipx install lives inside its own clean venv.
If you want five to ten CLI tools rather than libraries, pipx keeps the environment tidier than running pip install --break-system-packages over and over.
Choosing a pattern
- One-shot aggregation or experiment →
--break-system-packages - Recurring use in scheduled tasks →
python3 -m venv /tmp/venv - Multiple CLI tools →
pipx install - Strict dependency resolution → venv (or
uv)
For new projects I have been reaching for uv lately. It keeps a pip-compatible interface but creates venvs and installs packages dramatically faster. In a Cowork sandbox that rebuilds environments every run, raw startup speed translates directly into shorter task runtimes.
pip install uv --break-system-packages
uv venv /tmp/venv
uv pip install -p /tmp/venv/bin/python pandasThe need to bootstrap uv itself with --break-system-packages is the same situation as pipx.
Common gotchas
--user does not save you: pip install --user pandas looks like a clean escape into $HOME/.local, but PEP 668 blocks user installs too. You still need --break-system-packages.
sudo pip install is the worst option: It feels intuitive to try sudo when blocked, but PEP 668's protection is designed with root in mind first. The combination most likely to overwrite apt-managed packages is sudo --break-system-packages. Avoid it outside disposable VMs like Cowork.
venv built but packages invisible: You probably ran source in a separate bash call. Cowork's bash calls are independent, so source does not persist. Keep it in one call, or call python3 by full path.
pip install --upgrade pip rejected before requirements.txt: Upgrading pip itself is also blocked by PEP 668. Get into the venv first, then upgrade freely.
What to do next
When you hit this error, first confirm it really is PEP 668 by checking for the marker file: ls /usr/lib/python3.*/EXTERNALLY-MANAGED. The wording may look similar to other errors. Once confirmed, pick exactly one pattern from the list above that matches your use case. Reaching for "venv for everything" by default tends to be overkill for one-shot experiments.
I leaned heavily on --break-system-packages at first, but as scheduled tasks accumulated, venv became the calmer choice for daily operations. I hope this helps if you hit the same wall.