Skip to content

Install universal secrets-read guard hook for Claude, Cursor, and Codex#8

Open
njaltran wants to merge 1 commit into
develfrom
feat/secrets-guard-hook
Open

Install universal secrets-read guard hook for Claude, Cursor, and Codex#8
njaltran wants to merge 1 commit into
develfrom
feat/secrets-guard-hook

Conversation

@njaltran

@njaltran njaltran commented Jul 3, 2026

Copy link
Copy Markdown

Summary

dlthub-init now installs a secrets-read guard hook into every scaffolded workspace, for all three supported agents. The guard blocks direct reads of dlt secrets (secrets.toml, *.secrets.toml) and dotenv files (.env, .env.* except .env.example/.env.template/.env.sample) — turning the setup-secrets skill's prompt-level policy into technical enforcement. Since the scaffold ships .dlt/secrets.toml, protection is active from the first prompt.

Companion workbench PR (source of truth for the script): dlt-hub/dlthub-ai-workbench#79.

Design

One universal script, three agents. secrets_guard.py is stdlib-only, single-file, no imports — it detects the calling agent's dialect from the stdin payload (Cursor by its hook_event_name values, Claude/Codex by tool_name) and answers in that dialect. It follows the exact bundling pattern skills already use: repo-root hooks/ (synced from the workbench) → force-included in the wheel as _bundled_hooks/ → copied to the workspace's .agents/hooks/ at scaffold time.

Per-agent configs, one target:

Agent Config written Events
Claude Code .claude/settings.json PreToolUse, matcher Read|Grep|Bash
Cursor .cursor/hooks.json beforeReadFile + beforeShellExecution
Codex .codex/hooks.json PreToolUse, matcher Bash only (no dedicated Read/Grep tool — file access goes through shell)

Key decisions / tradeoffs

  • Path resolution differs per agent. No common cwd guarantee exists for hook processes. Claude gets "$CLAUDE_PROJECT_DIR"; Cursor/Codex expose no equivalent, so their commands resolve the project root via "$(git rev-parse --show-toplevel 2>/dev/null || pwd)". Works anywhere inside the repo; falls back to cwd for non-git workspaces.
  • Codex config goes in .codex/hooks.json, not .codex/config.toml [hooks] — repo-local config.toml hooks don't fire in interactive sessions (open upstream bug codex_hooks do not fire in interactive sessions when configured via repo-local .codex/config.toml openai/codex#17532); hooks.json is confirmed working.
  • Merge, never clobber. Config writes deep-merge into existing JSON (a user's existing .claude/settings.json permissions survive), skip any agent already referencing the guard (idempotent re-runs), and leave files with invalid JSON completely untouched.
  • Never overwrites a user-owned script. If .agents/hooks/secrets_guard.py already exists, it's kept — same no-clobber rule as skills.
  • Sync tolerates the pinned-ref gap. generate_skills.py now also syncs workbench/init/hooks/*.py, but the current pinned WORKBENCH_REF predates the hooks directory (workbench PR #79 not yet merged), so the initial hooks/secrets_guard.py is committed manually and the sync script warns-and-keeps rather than failing when hooks are absent at the ref. After #79 merges, make update-skills takes over normally.
  • Fail-open guard. Any error in the script itself (bad stdin, unexpected shape) allows the tool call — a guard bug must never brick normal file access. Not a sandbox: obfuscated access can bypass token matching; this is a deterrent for the common case.

Test plan

  • 11 new unit tests (tests/test_hooks.py): three-agent config generation, merge-preserves-existing-keys, idempotency, invalid-JSON untouched, user-owned script preserved, bundled/source resolution — full suite 113 tests green
  • make lint (ruff + mypy) and make format-check clean
  • End-to-end: ran the real CLI (dlthub-init --no-sync -y <tmp>), then executed the exact command strings from each generated config — Cursor dialect returned {"permission": "deny", ...} for .dlt/secrets.toml, Claude dialect returned the hookSpecificOutput deny for cat .env, with $CLAUDE_PROJECT_DIR / git rev-parse resolution both working
  • Same script live smoke-tested inside a real Claude Code session on the workbench side (real Read(".env") denied)

Bundles the workbench's secrets_guard.py (one stdlib-only script that
answers in each agent's hook dialect) and registers it at scaffold time:
.claude/settings.json (PreToolUse, Read|Grep|Bash), .cursor/hooks.json
(beforeReadFile/beforeShellExecution), and .codex/hooks.json (PreToolUse,
Bash only — Codex reads files via shell). All configs point at one copy
in .agents/hooks/, mirroring the skills install pattern.

Config writes merge into existing JSON without clobbering unrelated keys,
skip agents already referencing the guard, and leave invalid JSON files
untouched. Cursor/Codex commands locate the project root via git
rev-parse (no CLAUDE_PROJECT_DIR equivalent exists there); Codex uses
hooks.json rather than config.toml [hooks] due to openai/codex#17532.

generate_skills.py now also syncs workbench/init/hooks/*.py into hooks/,
tolerating pinned refs that predate the hooks directory; the initial
hooks/secrets_guard.py is committed manually until the workbench PR
(dlt-hub/dlthub-ai-workbench#79) merges and the ref is bumped.
Copilot AI review requested due to automatic review settings July 3, 2026 11:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

njaltran added a commit to dlt-hub/dlthub-ai-workbench that referenced this pull request Jul 3, 2026
dlt-hub/dlthub-init#8 registers the guard for all three agents at
scaffold time, generating config shapes in its hooks.py. Keeping
template JSONs here would duplicate that definition and drift. The
workbench now ships only the script, the Claude plugin hooks.json, and
docs; README notes the harmless double-registration when a project uses
both the scaffolder and the marketplace plugin.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants