# Codex message filter — a `UserPromptSubmit` hook prototype

A tiny, runnable prototype for the netsi.dk tutorial
**"Could Codex hooks filter messages before you read them?"**

It attaches a classifier to Codex's `UserPromptSubmit` lifecycle hook. When you
submit a message as a prompt, the hook classifies it as `fraud`, `political`, or
`neutral`, and (optionally) blocks the ones you choose.

> Verified: Codex has a hooks engine and `UserPromptSubmit` is a real, shipped
> hook event (OpenAI's "Claude Code Hook Parity" effort, hooks engine since
> ~v0.114.0). Always confirm the current schema in the official docs:
> https://developers.openai.com/codex/hooks

## Files

- `classify_message.py` — the classifier (offline heuristics by default)
- `classifier-prompt.txt` — the LLM classifier prompt (optional LLM mode)
- `hooks.json` — the Codex hook configuration

## Install (project-local)

1. Copy the files into your project:

   ```
   mkdir -p .codex/hooks
   cp classify_message.py .codex/hooks/classify_message.py
   cp classifier-prompt.txt .codex/hooks/classifier-prompt.txt
   cp hooks.json .codex/hooks.json
   chmod +x .codex/hooks/classify_message.py
   ```

   (For all projects instead, use `~/.codex/hooks.json` and
   `~/.codex/hooks/…`. Codex also supports an inline `[hooks]` table in
   `config.toml`.)

2. Make sure you are on a Codex CLI version that includes the hooks engine. If
   hooks are gated behind a feature flag in your version, enable them per the
   official docs (https://developers.openai.com/codex/hooks) — e.g. a
   `[features]` entry in `~/.codex/config.toml`.

## Test it

Pick which categories should block, then run Codex and submit each test message
as your prompt:

```
export NETSI_BLOCK="fraud,political"
```

| Test message | Expected category | With NETSI_BLOCK=fraud,political |
|---|---|---|
| "Are we still on for lunch at 12?" | neutral | allowed |
| "Vote YES on Tuesday — re-elect the mayor!" | political | blocked |
| "Your bank account is locked. Verify now: http://bit.ly/x" | fraud | blocked |
| "Skal vi spise frokost kl. 12?" | neutral | allowed |
| "Stem JA på torsdag så borgmesteren bliver genvalgt" | political | blocked |
| "Din konto er spærret. Bekræft din identitet med MitID her" | fraud | blocked |

The heuristics cover English and Danish. Inside a hook the script prints
nothing when a message is allowed (on `exit 0`, STDOUT would be added to the
agent's context); it only writes the block reason to STDERR. Run by hand, it
prints the category as shown below.

You can also run the classifier directly, without Codex:

```
echo '{"prompt":"Your account is locked, verify now http://bit.ly/x"}' | python3 classify_message.py
# -> fraud   (exit code 2 if fraud is in NETSI_BLOCK)

echo "Are we still on for lunch?" | python3 classify_message.py
# -> neutral
```

## Optional: use a real LLM classifier

```
pip install openai
export NETSI_USE_OPENAI=1
export OPENAI_API_KEY=sk-...
# export NETSI_MODEL=gpt-5-mini   # adjust to a model your account can use
```

The LLM path is illustrative — verify the model id and SDK call against the
current OpenAI API docs for your account.

## Important boundary — PROPOSED ARCHITECTURE

A `UserPromptSubmit` hook fires on the prompt **you** submit inside a Codex
session. Codex does **not** gain access to your SMS or iMessage because a hook
exists. Turning this into a real phone filter needs a separate, permissioned
OS/messaging integration that forwards the incoming message into this classifier.
That integration is the hard part and is **not** provided here.

## Sources

- Matthew Berman's LinkedIn question: https://lnkd.in/p/e3Qggkjy
- Codex hooks (official): https://developers.openai.com/codex/hooks
- Codex config (official): https://github.com/openai/codex/blob/main/docs/config.md
