plugin-dev: SQL injection in the documented `Database Logging` hook example

Author: SmartNightlyCreated Sep 16, 2026Updated Sep 16, 2026

The Database Logging example in the hook-authoring reference interpolates the raw hook payload into a SQL string literal. It is presented as a recommended pattern, so the expected outcome is that people paste it into a real PreToolUse hook pointed at a real $DATABASE_URL.

File: plugins/plugin-dev/skills/hook-development/references/advanced.md, §Database Logging (L287), code at L294. Confirmed against main at da64a66.

bash
input=$(cat)

# Log to database
psql "$DATABASE_URL" -c "INSERT INTO hook_logs (event, data) VALUES ('PreToolUse', '$input')" \
  2>/dev/null

$input is the raw hook payload read from stdin, interpolated unescaped into a single-quoted SQL literal. Hook input is JSON describing the tool call, so it routinely carries user- and file-derived content. A single ' anywhere in it terminates the literal and the remainder is parsed as SQL — a file path like /tmp/o'brien/x.ts, a Bash command containing an apostrophe, a commit message, or a string literal inside file content passed to Write.

Concretely, a Write whose content contains:

'); DROP TABLE hook_logs; --

closes the VALUES clause and executes the next statement. psql -c accepts multiple statements in one string, and 2>/dev/null hides the resulting errors, so a malformed or malicious payload fails silently.

Suggested fix

Let psql quote the value instead of interpolating it into the string yourself. Note that this needs the script on stdin rather than -c: -c takes "a command string that is completely parsable by the server", so psql variables are not interpolated there.

bash
input=$(cat)

psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -v payload="$input" <<'SQL'
INSERT INTO hook_logs (event, data) VALUES ('PreToolUse', :'payload');
SQL

:'payload' expands to the value quoted and escaped as a SQL literal, so embedded quotes are handled. The payload crosses into psql as a variable rather than as text spliced into the query, and the quoted heredoc (<<'SQL') keeps the shell out of it.

Dropping 2>/dev/null would also make failures visible.

Related, lower severity — same file

  • L358, Audit Logging: echo "$timestamp | $USER | $tool_name | $input" >> ~/.claude/audit.log writes the full raw payload — which can include file contents and any credentials passing through a tool call — to a plaintext file. Worth a one-line note about what lands there.
  • L269–L311: the Slack and statsd examples forward payload-derived data to external endpoints with no warning about what is being sent off-machine.

How this was found

Scanning my locally installed skills with NVIDIA SkillSpector v2.11.2:

bash
uv tool install git+https://github.com/NVIDIA/skillspector.git
skillspector scan <path>/plugins/plugin-dev/skills/hook-development

This surfaced in the scanner's LLM pass. For calibration: it was one of only two substantive findings across 31 scanned skills — the static stage is very noisy on documentation skills, and the LLM stage suppressed none of that noise. I verified every finding by hand; the rest were false positives.

Source: anthropics/claude-plugins-official