The README marks Audit Required for auto and envelope_only approval modes, but the audit subsystem is unreachable on the live code path, independent of approval mode. Distinct from #34 (Codex-misdetection permission-gate bypass, now fixed). One of three independent defects found in the same review of the v3.2.3 router, filed separately per this repo's one-defect-per-issue convention. Companions: #38, #39. Filed publicly since this is a missing feature wiring rather than an exploitable weakness — happy to re-route through security@snowflake.com per SECURITY.md if you'd prefer.
Summary
The plugin ships a complete, genuinely tamper-evident audit logger (scripts/router/security/audit_logger.py — a SHA-256 hash chain where each entry carries the previous entry's hash, so any edit or deletion breaks the chain), and the README advertises "structured audit logging" as a built-in protection (README.md:74), marking Audit Required for the auto and envelope_only approval modes (README.md:62-66). But no user-reachable code path ever writes it: the headless entrypoint both skills actually invoke, scripts/router/execute_cortex.py (584 lines at current main), contains zero references to audit, AuditLogger, or security_wrapper. The audit logger is only ever instantiated inside scripts/router/security_wrapper.py, which nothing in the shipped plugin imports or calls — and whose own execution step is a stub.
Expected behavior
A headless session produces an audit trail at the configured security.audit_log_path (default ~/.claude/skills/cortex-code/audit.log, per scripts/router/security/config_manager.py:34).
Actual behavior
Nothing is written. Empirical confirmation, after two full sessions (one RW executing real DDL, one RO/RESEARCH attempting reads) against a demo account, with no config override of security.audit_log_path present: ls ~/.claude/skills/cortex-code/ fails with "No such file or directory" — the default path's parent directory was never created. Since AuditLogger.__init__ does log_path.parent.mkdir(parents=True, exist_ok=True), the directory's absence proves the logger was never instantiated with the default path. find ~/.claude -iname audit.log returns nothing.
The per-tool-call decisions that should be audited do flow through this exact entrypoint — the --permission-prompt-tool stdio handler emits one allow/deny decision per tool call (e.g. {"tool_name":"Bash","action":"execute_command","resource":"snow sql ... -q \"create ...\"","behavior":"allow","reason":"RW allows Bash"}) — and every one of them is currently unrecorded.
Code
scripts/router/execute_cortex.py:19-21 (line numbers per current main, 8633372) — the entrypoint's only policy wiring is envelope_policy.decide; there is no audit import:
sys.path.insert(0, str(Path(__file__).parent))
from envelope_policy import decide as envelope_decide
from session_state import load_active_session, save_active_session
scripts/router/security_wrapper.py:26,86-90,216 — the logger exists, is instantiated, and is called, but only here:
from security.audit_logger import AuditLogger
# ...
audit_logger = AuditLogger(
log_path=audit_log_path,
rotation_size=audit_log_rotation,
retention_days=audit_log_retention
)
# ...
audit_id = audit_logger.log_execution(...)
And security_wrapper.py's own execution step is a stub (security_wrapper.py:206-212), so even wiring the wrapper in wouldn't run Cortex:
# Step 11: Execute with Cortex (simplified for now - actual execution via execute_cortex.py would go here)
# For now, return success with mock execution
execution_result = {
"status": "success",
"message": "Execution simulated (full Cortex integration in next phase)",
Lineage
This looks like wiring lost in a port rather than an intended omission: PR #12 ("Bundle router skill into cortex-code plugin with auto-routing") introduced execute_cortex.py importing only stdlib modules, while shipping audit_logger.py/security_wrapper.py as siblings it never calls, and no later commit added the wiring.
Reproduction (generic)
- Install v3.2.3+ with any working Snowflake connection.
- Run any prompt through the router's headless path (executes
scripts/router/execute_cortex.py), in any envelope.
ls ~/.claude/skills/cortex-code/audit.log — does not exist; the directory was never created.
find ~/.claude -iname audit.log — nothing.
grep -in "audit\|security_wrapper" scripts/router/execute_cortex.py — zero matches.
Suggested fix
Since security_wrapper.py's execution step is still the "simplified for now" stub above, is the intended path to complete that integration, or would you take wiring AuditLogger directly into execute_cortex.py? The direct wiring looks minimal: instantiate it from ConfigManager values (security.audit_log_path / audit_log_rotation / audit_log_retention, as security_wrapper.py:77-90 already does) at the start of execute_cortex_streaming(), then log each control_request → envelope_policy.decide() → _send_control_response() cycle plus a session-level log_execution record on completion.
The README marks Audit Required for
autoandenvelope_onlyapproval modes, but the audit subsystem is unreachable on the live code path, independent of approval mode. Distinct from #34 (Codex-misdetection permission-gate bypass, now fixed). One of three independent defects found in the same review of the v3.2.3 router, filed separately per this repo's one-defect-per-issue convention. Companions: #38, #39. Filed publicly since this is a missing feature wiring rather than an exploitable weakness — happy to re-route through security@snowflake.com per SECURITY.md if you'd prefer.Summary
The plugin ships a complete, genuinely tamper-evident audit logger (
scripts/router/security/audit_logger.py— a SHA-256 hash chain where each entry carries the previous entry's hash, so any edit or deletion breaks the chain), and the README advertises "structured audit logging" as a built-in protection (README.md:74), marking Audit Required for theautoandenvelope_onlyapproval modes (README.md:62-66). But no user-reachable code path ever writes it: the headless entrypoint both skills actually invoke,scripts/router/execute_cortex.py(584 lines at currentmain), contains zero references toaudit,AuditLogger, orsecurity_wrapper. The audit logger is only ever instantiated insidescripts/router/security_wrapper.py, which nothing in the shipped plugin imports or calls — and whose own execution step is a stub.Expected behavior
A headless session produces an audit trail at the configured
security.audit_log_path(default~/.claude/skills/cortex-code/audit.log, perscripts/router/security/config_manager.py:34).Actual behavior
Nothing is written. Empirical confirmation, after two full sessions (one RW executing real DDL, one RO/RESEARCH attempting reads) against a demo account, with no config override of
security.audit_log_pathpresent:ls ~/.claude/skills/cortex-code/fails with "No such file or directory" — the default path's parent directory was never created. SinceAuditLogger.__init__doeslog_path.parent.mkdir(parents=True, exist_ok=True), the directory's absence proves the logger was never instantiated with the default path.find ~/.claude -iname audit.logreturns nothing.The per-tool-call decisions that should be audited do flow through this exact entrypoint — the
--permission-prompt-tool stdiohandler emits one allow/deny decision per tool call (e.g.{"tool_name":"Bash","action":"execute_command","resource":"snow sql ... -q \"create ...\"","behavior":"allow","reason":"RW allows Bash"}) — and every one of them is currently unrecorded.Code
scripts/router/execute_cortex.py:19-21(line numbers per currentmain,8633372) — the entrypoint's only policy wiring isenvelope_policy.decide; there is no audit import:scripts/router/security_wrapper.py:26,86-90,216— the logger exists, is instantiated, and is called, but only here:And
security_wrapper.py's own execution step is a stub (security_wrapper.py:206-212), so even wiring the wrapper in wouldn't run Cortex:Lineage
This looks like wiring lost in a port rather than an intended omission: PR #12 ("Bundle router skill into cortex-code plugin with auto-routing") introduced
execute_cortex.pyimporting only stdlib modules, while shippingaudit_logger.py/security_wrapper.pyas siblings it never calls, and no later commit added the wiring.Reproduction (generic)
scripts/router/execute_cortex.py), in any envelope.ls ~/.claude/skills/cortex-code/audit.log— does not exist; the directory was never created.find ~/.claude -iname audit.log— nothing.grep -in "audit\|security_wrapper" scripts/router/execute_cortex.py— zero matches.Suggested fix
Since
security_wrapper.py's execution step is still the "simplified for now" stub above, is the intended path to complete that integration, or would you take wiringAuditLoggerdirectly intoexecute_cortex.py? The direct wiring looks minimal: instantiate it fromConfigManagervalues (security.audit_log_path/audit_log_rotation/audit_log_retention, assecurity_wrapper.py:77-90already does) at the start ofexecute_cortex_streaming(), then log eachcontrol_request→envelope_policy.decide()→_send_control_response()cycle plus a session-levellog_executionrecord on completion.