Problem
claude-hooks/block_install_deps.sh enforces FOUNDATION §2 ("agents must not install dependencies"), but its match is scoped to bare pip/conda at command start:
(^|[;&|]\s*)((python[0-9.]*\s+-m\s+)?pip[0-9.]*|conda) (install|create|env create|update)
This misses pipenv entirely — which is exactly how Option-A (pipenv) consumer repos install/resolve deps (the forge migration recipe explicitly supports pipenv):
pipenv install, pipenv sync, pipenv lock, pipenv update — none match (pipenv ≠ pip; after pip comes env, not a space/digit). All of these mutate the environment and/or re-resolve dependencies.
pipenv run pip install <x> — slips through because pip install isn't at command start (a space precedes it after run). This is the same documented "accepted slip-through" as xargs pip install.
Impact
In a pipenv repo an agent can freely re-resolve and bump unpinned deps — the precise env-breakage failure mode §2 exists to prevent. Observed in practice during a real migration: an agent ran pipenv lock to add one dev tool, which silently re-resolved ~80 unpinned packages to latest (transformers 5.4→5.12, datasets 4→5, torch 2.11→2.12, mypy 1→2…) and broke GPT2LMHeadModel import. The guardrail never fired.
Proposal
Extend block_install_deps.sh to also catch pipenv (and ideally other modern managers):
pipenv (install|sync|lock|update|uninstall) — match pipenv as a command word at start / after ;&|.
- Consider
uv pip install, uv add, uv sync, poetry (add|install|update|lock) for completeness.
- For the wrapper case (
pipenv run pip install …, uv run pip install …): catching pip install after a run wrapper would also catch it, but risks re-triggering on quoted bodies (the reason the current anchor is start-only). Safer to match the wrapper forms explicitly ((pipenv|uv|poetry)\s+run\s+pip\s+install) than to broaden the global pip anchor.
Make blocking configurable (default-on)
The block itself should be opt-out-able via [tool.forge] config, not hardcoded. Some repos legitimately want agents to run setup (sandboxed CI, throwaway envs, a trusted local flow), and a flat block forces ! -prefixing every pipenv sync. Proposal:
[tool.forge.hooks]
block_install_deps = true # default; set false to allow
# or per-manager granularity:
# block_install_deps = ["pip", "conda", "pipenv"] # omit "pipenv" to allow it
Default stays on (safe baseline, matches FOUNDATION §2). Consumers opt out deliberately. This also gives pipenv repos an escape hatch while the matcher above is tightened.
Notes
- Read-only commands should stay allowed (
pipenv --version, pipenv graph, pipenv run <non-install>), mirroring the existing pip/conda read-only allowlist.
- Pairs with the FOUNDATION §2 "fail loudly" intent: the block message already tells the user to run it themselves with
! <command> — same UX should apply to pipenv.
Problem
claude-hooks/block_install_deps.shenforces FOUNDATION §2 ("agents must not install dependencies"), but its match is scoped to bare pip/conda at command start:This misses pipenv entirely — which is exactly how Option-A (pipenv) consumer repos install/resolve deps (the forge migration recipe explicitly supports pipenv):
pipenv install,pipenv sync,pipenv lock,pipenv update— none match (pipenv≠pip; afterpipcomesenv, not a space/digit). All of these mutate the environment and/or re-resolve dependencies.pipenv run pip install <x>— slips through becausepip installisn't at command start (a space precedes it afterrun). This is the same documented "accepted slip-through" asxargs pip install.Impact
In a pipenv repo an agent can freely re-resolve and bump unpinned deps — the precise env-breakage failure mode §2 exists to prevent. Observed in practice during a real migration: an agent ran
pipenv lockto add one dev tool, which silently re-resolved ~80 unpinned packages to latest (transformers 5.4→5.12, datasets 4→5, torch 2.11→2.12, mypy 1→2…) and brokeGPT2LMHeadModelimport. The guardrail never fired.Proposal
Extend
block_install_deps.shto also catch pipenv (and ideally other modern managers):pipenv (install|sync|lock|update|uninstall)— matchpipenvas a command word at start / after;&|.uv pip install,uv add,uv sync,poetry (add|install|update|lock)for completeness.pipenv run pip install …,uv run pip install …): catchingpip installafter arunwrapper would also catch it, but risks re-triggering on quoted bodies (the reason the current anchor is start-only). Safer to match the wrapper forms explicitly ((pipenv|uv|poetry)\s+run\s+pip\s+install) than to broaden the globalpipanchor.Make blocking configurable (default-on)
The block itself should be opt-out-able via
[tool.forge]config, not hardcoded. Some repos legitimately want agents to run setup (sandboxed CI, throwaway envs, a trusted local flow), and a flat block forces!-prefixing everypipenv sync. Proposal:Default stays on (safe baseline, matches FOUNDATION §2). Consumers opt out deliberately. This also gives pipenv repos an escape hatch while the matcher above is tightened.
Notes
pipenv --version,pipenv graph,pipenv run <non-install>), mirroring the existing pip/conda read-only allowlist.! <command>— same UX should apply to pipenv.