Could Codex hooks filter messages before you read them?
Matthew Berman asked why he can't attach his own prompt to scan incoming text messages for obvious scams and political content before he reads them. This page fact-checks whether an OpenAI Codex lifecycle hook could be part of the answer — and gives you runnable code for both Codex and Claude Code.
A user-controlled AI pre-read layer
A prompt tells a model what to do. A lifecycle hook decides when something runs. Put the two together and you get a programmable layer that sees content at a defined moment and can label, annotate, or block it before it reaches you. Matthew's question is really the bigger one: why shouldn't users attach their own AI policy to the information streams they receive?
Proposed architecture
The full pipeline is not something Codex does on its own. Codex never gains access to SMS or iMessage because a hook exists. A separate, permissioned OS/messaging integration must legally and technically expose the incoming message and feed it to the AI layer. Codex is only explored here as the programmable AI processing layer in the middle.
What Codex can actually do
- Codex has a hooks engine (added ~v0.114.0, March 2026) built toward "Claude Code hook parity".
UserPromptSubmitis a real, shipped hook event — it fires when a prompt is submitted into a Codex turn.- A command hook receives the turn input and can run any script — deterministic or model-backed.
- On
UserPromptSubmit, exit code 2 rejects the prompt and shows the hook's stderr; the hook can also add context. - Hooks are configured per layer:
~/.codex/hooks.json(user),<repo>/.codex/hooks.json(project), an inline[hooks]table inconfig.toml, or managed/enterprise layers viarequirements.toml.
What Codex cannot do
- It cannot read your phone's messages. A hook only sees what enters a Codex session.
- It is not an iOS/Android SMS or iMessage filter, and installing a hook does not grant Messages access.
- There is no official "plugin hooks" path like Claude Code's
hooks/hooks.json— Codex hooks live in the config layers above. - Delivering messages into the classifier (with permission, low latency and privacy) is a separate product problem, not a Codex feature.
The classifier
Keep the output deliberately tiny — the downstream system should receive a category, not an essay.
Analyze the current incoming text message and return exactly one category: fraud political neutral First, determine whether the message contains links, unsolicited offers, requests for personal or sensitive information, suspicious urgency, impersonation, payment requests, or known scam patterns. Next, determine whether the message promotes political messages, political parties, candidates, campaigns, ideological advocacy, election-related content, or political calls to action. Classification rules: 1. Return "fraud" if credible fraud or deception indicators are present. 2. Otherwise return "political" if the message contains political advocacy, campaigning, persuasion, or a political call to action. 3. Otherwise return "neutral". Return only the category name.
The smallest working Codex prototype
Configure the hook. This structure is the one Codex's hooks engine uses (confirm the current schema in the official Codex hooks docs ↗). Save it as .codex/hooks.json in your project (or ~/.codex/hooks.json globally):
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 .codex/hooks/classify_message.py",
"timeoutSec": 10,
"statusMessage": "Scanning message"
}
]
}
]
}
}The script is the bridge: read the submitted text from the hook payload, classify it, and use exit 2 to block the categories you don't want. Inside a hook it stays silent when a message is allowed, because on exit 0 anything printed to stdout is added to the agent's context. The downloadable classify_message.py runs offline with English and Danish heuristics (no API key), and has an optional LLM mode. Core of it:
category = classify(message) # "fraud" | "political" | "neutral"
as_hook = '"hook_event_name"' in raw # silent inside a hook on exit 0
blocking = set(os.environ.get("NETSI_BLOCK", "").split(","))
if category in blocking:
print(f"Message filter blocked this input (category: {category}).",
file=sys.stderr)
sys.exit(2) # UserPromptSubmit: exit 2 = reject
if not as_hook:
print(category) # standalone: show the labelVerified The test messages below were run through the actual downloadable script.
Get the code — Codex and Claude Code
Two self-contained bundles. Both include the classifier, the prompt, the hook config and a README with install instructions. The classifier is identical; only the hook wiring differs, because Codex and Claude Code both expose a UserPromptSubmit hook.
🤖 Codex bundle
Hook config in .codex/hooks.json, script in .codex/hooks/.
✳️ Claude Code bundle
Hook config in .claude/settings.json, script in .claude/hooks/.
# Codex (project-local) mkdir -p .codex/hooks cp classify_message.py classifier-prompt.txt .codex/hooks/ cp hooks.json .codex/hooks.json # Claude Code (project-local) mkdir -p .claude/hooks cp classify_message.py classifier-prompt.txt .claude/hooks/ # merge the "hooks" block from settings.json into .claude/settings.json # (it blocks fraud,political by default — edit NETSI_BLOCK in the command)
Test it with a few messages
Run the classifier directly, or submit each message as a prompt with the hook installed. With NETSI_BLOCK="fraud,political" (the Claude Code bundle's default; for Codex, export it before starting Codex), the fraud and political messages are rejected (exit 2); neutral passes.
| Message | Expected | Blocked? |
|---|---|---|
| "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 |
echo '{"prompt":"Your account is locked, verify now http://bit.ly/x"}' \
| python3 classify_message.py
# -> fraud (exit code 2 when fraud is in NETSI_BLOCK)
echo "Are we still on for lunch?" | python3 classify_message.py
# -> neutralDecide what each category means
fraud
Quarantine, warn, or require an explicit reveal. Never silently delete — classifiers are wrong sometimes.
political
Move to a filtered view or label before display, per the user's own rule.
neutral
Continue through the normal inbox path.
uncertain
In production, add a confidence threshold or an "unknown" path even though the demo uses three labels.
Limitations & false positives
- The offline classifier is keyword heuristics. A legitimate message with a link ("here's the invoice: …") can trip the fraud rule; a news digest can read as political. Treat results as hints, not verdicts.
- An LLM classifier is better at nuance but adds latency, cost and its own errors, and sends message text to a model provider.
- "Political" is culturally and contextually loaded — the boundary between news, opinion and advocacy is genuinely hard, and over-blocking is a real risk.
- Always fail open: if classification errors, let the message through rather than hiding it.
Privacy implications
- Message content is sensitive. The offline heuristic keeps text on-device; the LLM mode sends it to an API — be explicit about that trade-off with users.
- A real integration would read a person's private messages, so it needs informed consent, data minimization, local processing where possible, and no silent retention.
- Blocking or hiding messages can hide important ones (a real bank alert, a real civic notice). The user must stay in control of the policy.
What a real SMS/iMessage filter would require
Everything below is the missing, non-Codex part of the pipeline:
- A platform integration with permission to receive or forward messages — on iOS, an SMS filter / Message Filter app extension; on Android, the default SMS role or a carrier-level hook. These are constrained by each OS.
- A secure bridge that passes the message to the AI layer with low latency and a safe fallback.
- Clear user consent, on-device processing where possible, and a transparent, user-editable policy for each category.
The classification is the easy 10%. Secure, consented access to the message stream — and what the OS actually lets a user automate — is the hard 90%. That is exactly what makes Matthew's question a good one.
Where this was checked
- Matthew Berman's LinkedIn question: https://lnkd.in/p/e3Qggkjy ↗
- Codex hooks (official OpenAI docs): https://developers.openai.com/codex/hooks ↗
- Codex configuration & hook config layers (official repo): github.com/openai/codex · docs/config.md ↗
- "Full Claude Code Hook Parity" tracker, incl.
UserPromptSubmitstatus (official repo issue): github.com/openai/codex · issue #21753 ↗ - Claude Code hooks (official docs, for the Claude Code bundle): docs.claude.com · claude-code/hooks ↗