Run prettier on backlog folder
This commit is contained in:
@@ -3,22 +3,26 @@ id: ML-157
|
||||
title: Prevent pi from accessing sensitive files
|
||||
status: Done
|
||||
assignee: []
|
||||
created_date: '2026-05-03 13:30'
|
||||
updated_date: '2026-05-03 14:50'
|
||||
created_date: "2026-05-03 13:30"
|
||||
updated_date: "2026-05-03 14:50"
|
||||
labels: []
|
||||
dependencies: []
|
||||
references:
|
||||
- 'backlog://document/doc-2'
|
||||
- "backlog://document/doc-2"
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
|
||||
To prevent the pi harness from accidentally reading and sending sensitive data to the LLM, we need a declarative way to intercept problematic commands that interact with sensitive files that for example contain secrets.
|
||||
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
<!-- AC:BEGIN -->
|
||||
|
||||
- [x] #1 Pi cannot read `.env` files via the `read` tool — access is blocked with a notification in interactive mode
|
||||
- [x] #2 Pi cannot read files matching `*secret*` or `*credential*` patterns via the `read` tool
|
||||
- [x] #3 Pi cannot `cat .env` or `grep` inside `.ssh/` or `.aws/` via the `bash` tool
|
||||
@@ -31,6 +35,7 @@ To prevent the pi harness from accidentally reading and sending sensitive data t
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Objective Alignment
|
||||
@@ -44,6 +49,7 @@ The solution: a pi extension that intercepts `tool_call` events before execution
|
||||
**Chosen: Route A — `tool_call` event interception with JSON config**
|
||||
|
||||
This is the simplest viable approach:
|
||||
|
||||
- ~80 lines of TypeScript + a JSON config file
|
||||
- Uses pi's built-in `tool_call` event hook (no tool reimplementation)
|
||||
- Inherits built-in rendering and behavior for all tools
|
||||
@@ -88,15 +94,12 @@ Example config:
|
||||
".gnupg/",
|
||||
"~/.pi/agent/sessions/"
|
||||
],
|
||||
"blocked_commands": [
|
||||
"printenv",
|
||||
"\\benv\\b",
|
||||
"\\bset\\b"
|
||||
]
|
||||
"blocked_commands": ["printenv", "\\benv\\b", "\\bset\\b"]
|
||||
}
|
||||
```
|
||||
|
||||
Initial patterns cover:
|
||||
|
||||
- `.env` files: `.env`, `.envrc` (`.env.*` is NOT included — it would false-positive on `.env.example`, which must remain readable)
|
||||
- Key files: `*.pem`, `*.key`, `*.key.pub`
|
||||
- Secret/credential files: paths containing `secret`, `credential`, `credentials`
|
||||
@@ -113,6 +116,7 @@ Verification: File exists at `.pi/sensitive-paths.json` and is valid JSON.
|
||||
File: `.pi/extensions/sensitive-file-guard.ts`
|
||||
|
||||
Implementation:
|
||||
|
||||
1. Read `.pi/sensitive-paths.json` at extension load time (synchronous, using `readFileSync`)
|
||||
2. Compile `blocked_paths` entries into a regex matcher (glob → regex conversion, all patterns compiled with the case-insensitive `i` flag to handle case-insensitive filesystems like macOS):
|
||||
- `*` → `.*`
|
||||
@@ -127,7 +131,7 @@ Implementation:
|
||||
b. Check the resolved absolute path against the compiled blocked path regexes.
|
||||
- **Bash tool**: Two separate checks with distinct purposes:
|
||||
a. **Blocked path scan:** Search the raw `event.input.command` string for fragments that match blocked path patterns (e.g., `.env`, `.ssh/`, `.aws/`). This catches commands like `cat .env`, `grep foo ~/.ssh/config`, `cat /absolute/path/.env`. The scan is a simple substring/regex match against the command text — it does not parse shell syntax. This covers the most common accidental access patterns.
|
||||
b. **Blocked command check:** Test `event.input.command` against `blocked_commands` regexes (from config). These cover commands that leak secrets *without* a path argument in the command string (e.g., `printenv`, `env`, `set`, `export`). Each regex uses `\b` word-boundary anchors to avoid false positives (e.g., `env` should match `env` but not `environment`).
|
||||
b. **Blocked command check:** Test `event.input.command` against `blocked_commands` regexes (from config). These cover commands that leak secrets _without_ a path argument in the command string (e.g., `printenv`, `env`, `set`, `export`). Each regex uses `\b` word-boundary anchors to avoid false positives (e.g., `env` should match `env` but not `environment`).
|
||||
- **Blocking and notification:**
|
||||
- If match found and `ctx.hasUI` is true: show a warning notification via `ctx.ui.notify()`
|
||||
- If match found: return `{ block: true, reason: "Blocked sensitive path: <path or command>" }` (error message for non-interactive mode)
|
||||
@@ -135,25 +139,26 @@ Implementation:
|
||||
|
||||
**Edge Cases and Accepted Limitations:**
|
||||
|
||||
| Scenario | Handling | Rationale |
|
||||
|----------|----------|-----------|
|
||||
| `cat ".env"` (quoted) | Caught by path substring scan | Quotes don't hide the path fragment |
|
||||
| `cat ./.env` (relative with `./`) | Caught by path substring scan | `.env` fragment still present |
|
||||
| `cat /absolute/path/.env` | Caught by path substring scan | `.env` fragment still present |
|
||||
| `cat $HOME/project/.env` | Caught (if `.env` appears verbatim) | Variable expansion at start doesn't mask the path fragment |
|
||||
| `head .env`, `tail .env`, `less .env` | Caught by path substring scan | All contain `.env` fragment |
|
||||
| `echo $MY_SECRET` (no file access) | NOT caught | This reads from environment memory, not disk. PI would need separate treatment for this (out of scope) |
|
||||
| `cat $(echo .env)` (command substitution) | NOT caught | Requires shell parsing; accepted limitation for accidental-access use case |
|
||||
| `eval 'cat .env'` | NOT caught | Requires shell parsing; accepted limitation |
|
||||
| `sh -c 'cat .env'` | Caught by path substring scan | `.env` appears in the command string |
|
||||
| Case variants (`.ENV`, `.Env`) | Caught by case-insensitive regex (`i` flag) | macOS filesystems are case-insensitive |
|
||||
| Path traversal (`../.env`) | Caught by `path.resolve` + `path.normalize` | Resolved absolute path contains `.env` fragment |
|
||||
| Symlinks (`config -> ~/.aws/`) | NOT caught in initial implementation | Resolving symlinks requires I/O (`realpathSync`) per tool call. Documented limitation; users must not symlink sensitive directories into the project tree. Can be added later via a config option `followSymlinks: true` |
|
||||
| `/etc/passwd` (system file) | NOT caught unless pattern added to config | Config is the source of truth for blocked paths; system files are not blocked by default |
|
||||
| Scenario | Handling | Rationale |
|
||||
| ----------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `cat ".env"` (quoted) | Caught by path substring scan | Quotes don't hide the path fragment |
|
||||
| `cat ./.env` (relative with `./`) | Caught by path substring scan | `.env` fragment still present |
|
||||
| `cat /absolute/path/.env` | Caught by path substring scan | `.env` fragment still present |
|
||||
| `cat $HOME/project/.env` | Caught (if `.env` appears verbatim) | Variable expansion at start doesn't mask the path fragment |
|
||||
| `head .env`, `tail .env`, `less .env` | Caught by path substring scan | All contain `.env` fragment |
|
||||
| `echo $MY_SECRET` (no file access) | NOT caught | This reads from environment memory, not disk. PI would need separate treatment for this (out of scope) |
|
||||
| `cat $(echo .env)` (command substitution) | NOT caught | Requires shell parsing; accepted limitation for accidental-access use case |
|
||||
| `eval 'cat .env'` | NOT caught | Requires shell parsing; accepted limitation |
|
||||
| `sh -c 'cat .env'` | Caught by path substring scan | `.env` appears in the command string |
|
||||
| Case variants (`.ENV`, `.Env`) | Caught by case-insensitive regex (`i` flag) | macOS filesystems are case-insensitive |
|
||||
| Path traversal (`../.env`) | Caught by `path.resolve` + `path.normalize` | Resolved absolute path contains `.env` fragment |
|
||||
| Symlinks (`config -> ~/.aws/`) | NOT caught in initial implementation | Resolving symlinks requires I/O (`realpathSync`) per tool call. Documented limitation; users must not symlink sensitive directories into the project tree. Can be added later via a config option `followSymlinks: true` |
|
||||
| `/etc/passwd` (system file) | NOT caught unless pattern added to config | Config is the source of truth for blocked paths; system files are not blocked by default |
|
||||
|
||||
**Bash scanning scope:** The bash scanner uses simple substring/regex matching on the raw command string. It is designed to catch *accidental* access by the LLM, not adversarial bypass by a human. Commands using shell variable expansion, command substitution, or indirect execution may evade detection; this is an accepted limitation documented above.
|
||||
**Bash scanning scope:** The bash scanner uses simple substring/regex matching on the raw command string. It is designed to catch _accidental_ access by the LLM, not adversarial bypass by a human. Commands using shell variable expansion, command substitution, or indirect execution may evade detection; this is an accepted limitation documented above.
|
||||
|
||||
Verification:
|
||||
|
||||
- Start pi in this project, ask it to read `.env.example` (should work — this is NOT a secret, it's the example)
|
||||
- Ask pi to read `.env` (should be blocked with a notification)
|
||||
- Ask pi to run `cat .env` via bash (should be blocked)
|
||||
@@ -211,6 +216,7 @@ Verification: All tests pass before proceeding to integration verification.
|
||||
The extension must handle `ctx.hasUI === false` (print mode, JSON mode) by returning a descriptive error reason string, so the blocked access is reported to stdout rather than silently swallowed.
|
||||
|
||||
Verification:
|
||||
|
||||
```bash
|
||||
pi -p "read the .env file" 2>&1 | grep -i "blocked"
|
||||
# Should produce output indicating the access was blocked
|
||||
@@ -219,6 +225,7 @@ pi -p "read the .env file" 2>&1 | grep -i "blocked"
|
||||
**Step 4: Verify other tools are unaffected**
|
||||
|
||||
Ask pi to:
|
||||
|
||||
- Read a normal source file (e.g., `lib/music_library.ex`)
|
||||
- Edit a normal file
|
||||
- Run `mix test`
|
||||
@@ -232,41 +239,41 @@ Verification: All normal pi operations are unaffected.
|
||||
|
||||
Each step includes concrete verification instructions above. Overall verification suite:
|
||||
|
||||
| Test Case | Tool | Target | Expected |
|
||||
|-----------|------|--------|----------|
|
||||
| Read .env | read | `.env` | Blocked |
|
||||
| Read .env via path traversal | read | `../.env` | Blocked |
|
||||
| Read .env.production | read | `.env.production` | Blocked (if pattern added) |
|
||||
| Read .env.example | read | `.env.example` | **Allowed** (example file) |
|
||||
| Read .ENV (case variant) | read | `.ENV` | Blocked (case-insensitive) |
|
||||
| Read secrets file | read | `config/secrets.yml` | Blocked |
|
||||
| Cat .env via bash | bash | `cat .env` | Blocked |
|
||||
| Cat .env via bash with quotes | bash | `cat ".env"` | Blocked |
|
||||
| Cat ./.env via bash | bash | `cat ./.env` | Blocked |
|
||||
| Grep in .ssh | grep | `~/.ssh/config` | Blocked |
|
||||
| Ls in .aws | ls | `~/.aws/` | Blocked |
|
||||
| Write to .env | write | `.env` | Blocked |
|
||||
| Edit .env | edit | `.env` | Blocked |
|
||||
| Bash printenv (no path) | bash | `printenv` | Blocked |
|
||||
| Bash env command | bash | `env` | Blocked |
|
||||
| Bash echo $NODE_ENV (false positive check) | bash | `echo $NODE_ENV` | **Allowed** (`\b` anchor) |
|
||||
| Read normal .ex file | read | `lib/music_library.ex` | Allowed |
|
||||
| Run mix test | bash | `mix test` | Allowed |
|
||||
| Non-interactive read .env | read | `.env` (print mode) | Error reported |
|
||||
| Non-interactive read source file | read | `lib/music_library.ex` (print mode) | Normal output |
|
||||
| Add pattern + /reload | read | newly blocked path | Blocked after reload |
|
||||
| Test Case | Tool | Target | Expected |
|
||||
| ------------------------------------------ | ----- | ----------------------------------- | -------------------------- |
|
||||
| Read .env | read | `.env` | Blocked |
|
||||
| Read .env via path traversal | read | `../.env` | Blocked |
|
||||
| Read .env.production | read | `.env.production` | Blocked (if pattern added) |
|
||||
| Read .env.example | read | `.env.example` | **Allowed** (example file) |
|
||||
| Read .ENV (case variant) | read | `.ENV` | Blocked (case-insensitive) |
|
||||
| Read secrets file | read | `config/secrets.yml` | Blocked |
|
||||
| Cat .env via bash | bash | `cat .env` | Blocked |
|
||||
| Cat .env via bash with quotes | bash | `cat ".env"` | Blocked |
|
||||
| Cat ./.env via bash | bash | `cat ./.env` | Blocked |
|
||||
| Grep in .ssh | grep | `~/.ssh/config` | Blocked |
|
||||
| Ls in .aws | ls | `~/.aws/` | Blocked |
|
||||
| Write to .env | write | `.env` | Blocked |
|
||||
| Edit .env | edit | `.env` | Blocked |
|
||||
| Bash printenv (no path) | bash | `printenv` | Blocked |
|
||||
| Bash env command | bash | `env` | Blocked |
|
||||
| Bash echo $NODE_ENV (false positive check) | bash | `echo $NODE_ENV` | **Allowed** (`\b` anchor) |
|
||||
| Read normal .ex file | read | `lib/music_library.ex` | Allowed |
|
||||
| Run mix test | bash | `mix test` | Allowed |
|
||||
| Non-interactive read .env | read | `.env` (print mode) | Error reported |
|
||||
| Non-interactive read source file | read | `lib/music_library.ex` (print mode) | Normal output |
|
||||
| Add pattern + /reload | read | newly blocked path | Blocked after reload |
|
||||
|
||||
### 5. Architecture Impact Analysis
|
||||
|
||||
| Touchpoint | Impact |
|
||||
|------------|--------|
|
||||
| Touchpoint | Impact |
|
||||
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `.pi/extensions/sensitive-file-guard.ts` | **New file** — extension entry point. Named `000-sensitive-file-guard.ts` (or similar numeric prefix) to ensure it loads **first** among project-local extensions. Since `tool_call` handlers chain in load order and later handlers can mutate `event.input` before this guard sees it, loading first guarantees the guard inspects the original, unmodified tool arguments. |
|
||||
| `.pi/sensitive-paths.json` | **New file** — declarative blocked patterns config |
|
||||
| `tool_call` event | New subscriber, chains with existing extensions (e.g., MCP adapter). The guard runs **before** the MCP adapter's `tool_call` handler, so sensitive file access is blocked before any MCP forwarding occurs. |
|
||||
| Built-in tools | **Unchanged** — interception is non-invasive |
|
||||
| Phoenix/Ecto/Oban | **No impact** — pi-only concern, not part of the Elixir application |
|
||||
| Production infrastructure | **No impact** — this is developer-local tooling, not server configuration |
|
||||
| Other pi extensions (MCP adapter) | Compatible — `tool_call` handlers chain in load order. The guard runs alongside, not instead of. |
|
||||
| `.pi/sensitive-paths.json` | **New file** — declarative blocked patterns config |
|
||||
| `tool_call` event | New subscriber, chains with existing extensions (e.g., MCP adapter). The guard runs **before** the MCP adapter's `tool_call` handler, so sensitive file access is blocked before any MCP forwarding occurs. |
|
||||
| Built-in tools | **Unchanged** — interception is non-invasive |
|
||||
| Phoenix/Ecto/Oban | **No impact** — pi-only concern, not part of the Elixir application |
|
||||
| Production infrastructure | **No impact** — this is developer-local tooling, not server configuration |
|
||||
| Other pi extensions (MCP adapter) | Compatible — `tool_call` handlers chain in load order. The guard runs alongside, not instead of. |
|
||||
|
||||
**No migration or deprecation path needed** — this is a net-new capability.
|
||||
|
||||
@@ -283,6 +290,7 @@ Each step includes concrete verification instructions above. Overall verificatio
|
||||
### 7. Benchmarking Requirements
|
||||
|
||||
**No benchmarks needed.** The guard is a simple gate with constant-time pattern matching:
|
||||
|
||||
- O(20) regex tests per tool call
|
||||
- No I/O beyond the initial config read (done once at startup)
|
||||
- No allocations beyond the return value object
|
||||
@@ -293,6 +301,7 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co
|
||||
### 8. Cost Profile
|
||||
|
||||
**Zero cost.** The guard:
|
||||
|
||||
- Uses no third-party APIs
|
||||
- Consumes no paid resources
|
||||
- Runs entirely locally in the pi process
|
||||
@@ -314,6 +323,7 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co
|
||||
### 10. Documentation Updates
|
||||
|
||||
**`docs/project-conventions.md`** — Add a section "Pi Security Configuration" documenting:
|
||||
|
||||
- The existence of the sensitive file guard extension
|
||||
- The location and format of `.pi/sensitive-paths.json`
|
||||
- How to add or remove blocked patterns
|
||||
@@ -329,11 +339,13 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co
|
||||
**SKILL.md (optional)** — Consider creating `.claude/skills/sensitive-file-guard/SKILL.md` so pi itself understands the guard's behavior. Without this, pi may try to read a blocked file, receive the block reason in the next turn, and need to re-plan. A SKILL.md pre-loads this knowledge so pi avoids attempting blocked paths proactively. This is a nice-to-have, not required for the initial implementation.
|
||||
|
||||
**No other documentation files need updates.** This is a pi-level concern, not an application architecture concern.
|
||||
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
<!-- SECTION:NOTES:BEGIN -->
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Files Created
|
||||
@@ -350,16 +362,19 @@ If future patterns grow to hundreds of entries, a trie-based matcher could be co
|
||||
- **Fail-open**: If config file is missing or invalid, the extension silently does nothing.
|
||||
|
||||
### Key Limitation
|
||||
|
||||
- Does NOT resolve symlinks (would require `realpathSync` per tool call). Users must not symlink sensitive directories into the project tree.
|
||||
<!-- SECTION:NOTES:END -->
|
||||
|
||||
## Final Summary
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||
|
||||
Created two files:
|
||||
|
||||
1. **`.pi/sensitive-paths.json`** — Declarative config blocking `mise.local.toml` and other sensitive file patterns
|
||||
2. **`.pi/extensions/000-sensitive-file-guard.ts`** — Extension that intercepts `tool_call` events before any file I/O occurs, blocks matching paths/commands, notifies in interactive mode, and reports error reason in non-interactive mode.
|
||||
|
||||
The guard covers all built-in file-access tools (read, grep, write, edit, find, ls, bash) and loads first (000- prefix) among project-local extensions to inspect original tool arguments.
|
||||
|
||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||
|
||||
Reference in New Issue
Block a user