How Cal.com Rebuilt AppSec After Going Closed Source
How Cal.com consolidated noisy security tooling into one continuous, context-aware pull request security program with Gecko.
Gecko Security

When pip says “environment is externally managed”, it’s blocking you from installing packages into the system Python. You’ll see this on Ubuntu, macOS with Homebrew, Raspberry Pi, inside Docker, pretty much any modern OS that uses a package manager to control Python installations. The error originated with PEP 668 as a way to prevent pip from overwriting packages that system tools depend on, which used to cause silent breakage that was nearly impossible to debug. The solution isn’t always the same though, because what works for a throwaway CI container will break a long-lived server, and what makes sense for a development project doesn’t fit CLI tools you want available globally.
TLDR:
--break-system-packages flag exists for throwaway containers, not persistent systemsWhen you run pip install on Ubuntu, macOS, Raspberry Pi, or inside a Docker container, you may see something like this:
<code class="language-python">error: externally-managed-environment
<p>× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz
</code>This error originates from a marker file called EXTERNALLY-MANAGED, placed by your OS inside the Python installation directory. Its purpose is straightforward: tell pip not to touch the system Python environment. Per the Python Packaging Authority spec, this file signals that a separate package manager like apt or brew owns that interpreter.
Your OS depends on specific Python package versions to run system tools. A rogue pip install could silently break them. The error is a guardrail, not a bug.
Before 2022, nothing stopped pip from overwriting Python packages that your OS relied on. That created a quiet category of breakage that was hard to diagnose: system tools written in Python would fail, sometimes silently, after an unrelated pip install touched a shared dependency.
PEP 668 formalized a solution to this long-standing tension. The core conflict is straightforward: apt, brew, and similar tools manage packages as a coordinated set, with tested version combinations. Pip knows nothing about those constraints. When both tools write to the same directories, you get file ownership conflicts, unexpected downgrades, and broken system scripts.
The EXTERNALLY-MANAGED marker gives OS maintainers a way to enforce boundaries without patching pip itself.
Virtual environments give pip its own isolated space, completely separate from the system Python. No file ownership conflicts, no broken OS tools.
Here’s the full workflow:
<code class="language-bash">python3 -m venv .venv</p>
<p>source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows</p>
<p>pip install requests</p>
<p>deactivate
</code>Once activated, pip installs into .venv/lib instead of the system directories. The EXTERNALLY-MANAGED restriction never triggers because you’re no longer touching the OS-managed interpreter.
This works identically inside Docker, WSL, Raspberry Pi, and Conda setups. Each project gets its own dependency tree, and upgrades in one project never affect another.
Your OS package manager already has pre-tested builds of many popular libraries, and for common dependencies on shared systems, it’s often the cleanest path forward.
| OS | Command Example |
|---|---|
| Ubuntu/Debian | sudo apt install python3-requests |
| Arch/Manjaro | sudo pacman -S python-requests |
| Fedora/RHEL | sudo dnf install python3-requests |
| macOS (Homebrew) | brew install python-requests |
On Debian-based systems, the naming convention is typically python3- followed by the package name. If you’re unsure whether a package exists, run apt search python3- before assuming it’s unavailable.
The tradeoff is real though. Package managers lag behind PyPI on version freshness, and niche libraries often aren’t packaged at all. Where this approach wins is security: OS-managed packages receive automatic security patches through your normal system updates, with no manual upgrades required.
If you need a quick fix without setting up a virtual environment, pip exposes an escape hatch:
<code class="language-bash">pip install requests --break-system-packages
</code>To set it globally via pip config:
<code class="language-bash">pip config set global.break-system-packages true
</code>Be deliberate about when you reach for this. The flag exists for edge cases like ephemeral CI containers or one-off scripts where environment longevity doesn’t matter. On any persistent system, whether a personal laptop, a Raspberry Pi running services, or a shared Ubuntu server, it invites the exact dependency conflicts PEP 668 was designed to prevent.
The name isn’t dramatic. It’s accurate.
pipx sits in a useful middle ground: global availability without the system pollution. Each tool gets its own isolated virtual environment, created and managed automatically. You get the benefits of isolation without activating anything manually.
Install it first:
<code class="language-bash">sudo apt install pipx # Ubuntu/Debian
brew install pipx # macOS
pipx ensurepath
</code>Then install any Python CLI tool:
<code class="language-bash">pipx install youtube-dl
pipx install black
pipx install httpie
</code>The binary lands on your PATH. The dependencies stay sandboxed. Running pipx upgrade-all keeps everything current without touching system packages.
pipx fits best for tools you want available everywhere, like formatters, linters, or download utilities, where creating a per-project venv feels like overkill. For application dependencies, stick with regular virtual environments.
Each OS has slight quirks worth knowing. The fix is the same conceptually, but the commands differ.
Both ship with EXTERNALLY-MANAGED active by default since Bookworm and Noble. Use python3-venv to create environments:
<code class="language-bash">sudo apt install python3-venv
python3 -m venv .venv && source .venv/bin/activate
</code>Homebrew-managed Python triggers brew error: externally-managed-environment. Create a venv or use pipx. Never delete the EXTERNALLY-MANAGED file manually.
Same Debian base, same restriction. The venv workflow works identically. On Pi setups with tight memory, keep venvs lean.
WSL runs a full Linux distro, so Ubuntu instructions apply directly. No special handling needed.
Arch uses python- prefixes in pacman. For anything outside the repos, create a venv.
In Dockerfiles, add --break-system-packages only in ephemeral build stages, or set the base image to use a venv:
<code class="language-dockerfile">RUN python3 -m venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
RUN pip install -r requirements.txt
</code>Choosing the right fix depends on what you’re building and how long the environment needs to last.
| Scenario | Best Approach |
|---|---|
| Development project | Virtual environment |
| CLI tools (black, httpie) | pipx |
| Production deployment | Virtual environment or container |
| Ephemeral CI/CD build | --break-system-packages |
| Common system library | OS package manager |
If you’re writing application code, a virtual environment is always the right call. For CLI tools you want globally accessible, pipx is cleaner. Reach for the system package manager when a library is already packaged and version freshness doesn’t matter. Save --break-system-packages for throwaway containers only.
Running pip install globally means the package’s setup.py executes with whatever privileges you used. Run it with sudo, and a malicious package gets root access to your system.
Virtual environments sidestep this entirely. Installations run as your regular user, scoped to the project directory, so any compromise stays contained.
Dependency tracking matters too. Isolated environments make it straightforward to audit exactly what’s installed and at what version, which becomes relevant the moment you need to respond to a supply chain vulnerability. A bloated global environment makes that audit painful.
The EXTERNALLY-MANAGED restriction, frustrating as it feels, pushes you toward practices that security teams actually prefer.
Proper dependency management gets your environment clean. What it doesn’t do is catch the vulnerabilities that show up once those packages interact with your application logic.
Python makes this harder than it sounds. The language is dynamically typed, which means object types are not resolved until runtime. A variable holding a Django model today might hold a custom wrapper tomorrow. The actual method being called depends on what got passed in, not what the code looks like on the page. Call chains that matter for security analysis often do not resolve statically at all. This is a fundamental limitation, not of any one tool, but of the problem itself.
Traditional security scanners treat Python dependencies as a flat list. They check package versions against CVE databases, flag outdated libraries, and call it done. That misses the harder class of problems: authorization gaps, broken access controls, and multi-step logic flaws that live in how your code actually calls those packages, not in the packages themselves.
Most SAST tools try to bridge this gap with AST parsing, walking the syntax tree to identify function calls and trace data flow from what’s written in the source. The gap isn’t in parsing. AST analysis sees the structure of your code, not its behavior. When decorators wrap route handlers, when metaclasses change attribute resolution, when objects mutate across module boundaries, the AST gives you a skeleton. The actual runtime behavior is invisible to it. That’s exactly the kind of complexity where real vulnerabilities live.
Gecko uses Python-specific language servers instead of raw AST analysis. Language servers understand Python semantics, including type inference, import resolution, and method resolution order, the same way an IDE does when it autocompletes across module boundaries. That means call chains resolve correctly even when types are not explicit, false positives drop because the analysis knows what is actually reachable, and the vulnerabilities that matter surface instead of getting buried under noise. Whether a vulnerability lives inside a Django view, a FastAPI route handler, or a custom middleware layer wrapping a third-party library, the analysis follows the logic instead of stopping at a file boundary. Virtual environments help here too, since isolated dependency trees give the language server a clean, accurate picture of what is actually in scope.
If you want to see how that analysis works on a real codebase, try Gecko free.
What starts as an annoying externally managed environment pip error becomes muscle memory once you internalize the pattern. Virtual environments for projects, pipx for tools, system packages for shared libraries, and the break flag only when nothing else fits. Your dependency graph stays clean, your system stays stable, and security audits become possible. If you want to talk through how Gecko scans Python apps with complex dependency chains, book 30 minutes and we’ll show you the details.

Artemiy Malyshau
Co-founder & CTO
Artemiy served in an elite unit of the Austrian Cyber Forces, defending national infrastructure He was then the first employee at a government-backed cybersecurity research group, where he led security projects for Interpol and national governments. At Gecko he builds the platform trusted to sit inside Fortune 500 codebases, and holds it to the standard those governments taught him.
The latest news, technologies, and resources from our team.
How Cal.com consolidated noisy security tooling into one continuous, context-aware pull request security program with Gecko.
Gecko Security
Authorization bypass in n8n’s dynamic-credentials OAuth endpoints allows any authenticated user to operate on another user’s OAuth credential by supplying its ID, enabling unauthorized OAuth rebinding and revocation.
Artemiy Malyshau
An IDOR vulnerability in n8n’s public variables API allows authenticated users to read project variables outside their authorized scope, exposing secrets across project boundaries.
Artemiy Malyshau
Learn API scanning for automated security testing. Find vulnerabilities from broken authentication to business logic flaws in your endpoints.
Artemiy Malyshau
A complete guide to automated pentest tools and best practices. Learn what works, what doesn’t, and how to implement continuous security testing.
Artemiy Malyshau
Compare the best AI-powered application security testing tools. Find which tools detect business logic flaws and broken access control.
Artemiy Malyshau
Occasional updates, new content, and insights. No spam; unsubscribe anytime.