Skip to content
Back to skills

Sql Substitution

BSecurity

Implements SQL variable substitution (Oracle &varname, psql :variable, MySQL @var, SQLite3 .variable) with ACCEPT prompts, ampersand escaping, COALESCE defaults, and CI/CD-safe patterns.

  • 4 stars
  • 0 votes
  • 0 copies
  • 16 views
  • Added June 12, 2026
databasespythongorubyphpc++shellbashsqlgitapi

Works with

  • cursor
  • terminal
  • cli
  • api

Security analysis

B88/100
  • criticalSends environment variables or credentials to an external URL

Pro shows the line behind each finding and how to fix it

Scanned June 12, 2026

npx -y skills add paulpas/agent-skill-router --skill sql-substitution --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Sql Substitution?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Sql Substitution
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/paulpas-sql-substitution/badge)](https://www.skillsdirectory.com/skills/paulpas-sql-substitution)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---




name: sql-substitution
description: Implements SQL variable substitution (Oracle &varname, psql :variable, MySQL @var, SQLite3 .variable) with ACCEPT prompts, ampersand escaping, COALESCE defaults, and CI/CD-safe patterns.
archetypes:
  - tactical
  - generation
anti_triggers:
  - brainstorming
  - vague ideation
response_profile:
  verbosity: low
  directive_strength: high
  abstraction_level: operational
license: MIT
compatibility: opencode
metadata:
  version: "1.0.0"
  domain: coding
  triggers: SQL substitution, ampersand variable, Oracle substitution, psql variables, &varname, ACCEPT PROMPT, escaping ampersand, missing variable handling
  role: implementation
  scope: implementation
  output-format: code
  content-types: [code, guidance, do-dont, examples]
  related-skills: linux-shell-command-chaining, error-handling




---





# SQL Variable Substitution Across Database Tools

Implements interactive and scripted SQL variable substitution for Oracle SQL*Plus, PostgreSQL psql, MySQL CLI, and SQLite3. When loaded, the model produces correct variable definition prompts, substitution references, literal-ampersand escaping, missing-variable guards with COALESCE/NULLIF, and CI/CD-safe non-interactive patterns across all four database CLI environments.

## TL;DR Checklist

- [ ] Match the database dialect to its specific variable syntax (`&varname` for Oracle, `:variable` for psql, shell expansion for MySQL)
- [ ] Use `ACCEPT var PROMPT '...'` for interactive prompts in Oracle SQL*Plus scripts
- [ ] Prefix persistent variables with `&&` so they survive beyond a single statement execution
- [ ] Escape literal ampersands that must not trigger substitution (Oracle: `SET ESCAPE ON`, psql: `\set :variable` quoting)
- [ ] Wrap all substituted values in `COALESCE()` or guard with `NULLIF()` to handle missing/undefined variables gracefully
- [ ] Never use interactive ACCEPT prompts in CI/CD pipelines — pass variables via file, environment, or `-v` flags
- [ ] Validate every variable has a default or fallback before running production DML

---

## When to Use

Use this skill when:

- Writing SQL scripts that accept user input interactively (Oracle SQL*Plus `ACCEPT`, psql `\prompt`)
- Building ETL or data-generation scripts where table names, date ranges, or file paths are supplied at runtime
- Converting a hardcoded query into a parameterized script for reuse across environments (dev → staging → prod)
- Escaping literal ampersand characters in strings that must NOT trigger variable substitution (e.g., "C++&PHP")
- Hardening scripts against missing variables by providing defaults via `COALESCE('&var', 'default_value')`
- Automating SQL execution in CI/CD where interactive prompts would hang the pipeline

---

## When NOT to Use

Avoid this skill for:

- Application-layer queries — use your language's parameterized query API (e.g., Python `cursor.execute("SELECT * FROM t WHERE id = %s", [val])`) instead of CLI substitution
- High-performance production workloads where variable substitution adds parsing overhead — bind variables at the driver level are faster
- Simple one-off queries with no reuse potential — inline the values directly

---

## Core Workflow

1. **Identify the Database Dialect** — Determine which CLI tool runs your SQL: Oracle SQL*Plus, PostgreSQL psql, MySQL CLI, or SQLite3. Each has a completely different substitution syntax. **Checkpoint:** Confirm the target tool by checking `SELECT banner FROM v$version;` (Oracle), `\q` help text (psql), or `--help` flag availability.

2. **Choose Substitution Method** — Select the right approach for your use case:
   - Interactive prompts → Oracle `ACCEPT`, psql `\prompt`, MySQL shell variables
   - Pre-set values → Oracle `DEFINE`, psql `\set`, MySQL `mysql -e "SET @var=..."`, SQLite `.read` with `-cmd`
   - CI/CD non-interactive → environment variable export, heredoc, or config file injection
   **Checkpoint:** Ensure the chosen method works in your execution context (terminal vs. cron vs. GitHub Actions).

3. **Define Variables with Defaults** — Always provide a fallback value so missing variables don't cause errors. In Oracle: `DEFINE var = COALESCE('&var', 'default')`. In psql: `\set var :'DEFAULT_VAR'` then reference `:var`. **Checkpoint:** Test the script with the variable unset to confirm the default activates without hanging or throwing an error.

4. **Escape Literal Ampersands** — If your SQL data contains literal `&` characters that must not trigger substitution, configure the escape mechanism for your dialect before running any DML. **Checkpoint:** Run a test statement that includes "A&B" and verify it stores as "A&B", not as an expanded variable reference.

5. **Execute and Validate** — Run the script and confirm all substituted values produce the expected results. For CI/CD, capture output and assert non-zero exit codes on errors. **Checkpoint:** Review execution log for any `SP2-0552` (Oracle bind variable undefined) or `\1: variable "x" is not set` (psql) messages.

---

## Implementation Patterns

### Pattern 1: Oracle SQL*Plus ACCEPT for Interactive Prompts

The `ACCEPT` command prompts the user and stores input into a substitution variable. This is the primary interactive pattern in Oracle SQL*Plus scripts.

```sql
-- ❌ BAD: No prompt text, no default — user sees bare cursor with no guidance
ACCEPT myvar

SELECT * FROM employees WHERE department_id = &myvar;
```

```sql
-- ✅ GOOD: Clear prompt, data type hint, and sensible fallback via DEFINE
ACCEPT dept_id NUMBER PROMPT 'Enter department ID (or press Enter for 10): ' DEFAULT 10

ACCEPT report_date DATE PROMPT 'Report date (YYYY-MM-DD): ' FORMAT YYYY-MM-DD

SELECT employee_name, salary
  FROM employees
 WHERE department_id = NVL(&dept_id, 10)
   AND hire_date < TO_DATE('&report_date', 'YYYY-MM-DD');
```

```sql
-- ✅ GOOD: ACCEPT with validation via a wrapper script pattern
ACCEPT emp_id PROMPT 'Employee ID (must be numeric): '

BEGIN
  -- Guard clause: reject empty or non-numeric input
  IF '&emp_id' IS NULL OR '&emp_id' NOT LIKE '%[^0-9]%' THEN
    :output_msg := 'Processing employee: ' || &emp_id;
  ELSE
    RAISE_APPLICATION_ERROR(-20001, 'Invalid employee ID: &emp_id');
  END IF;
END;
/
```

### Pattern 2: Oracle Persistent Substitution (&&) vs. Single-Use (&)

Single `&` prompts each time the variable is referenced; double `&&` defines it once and reuses across the entire session.

```sql
-- ❌ BAD: Single & asks user for start_date on every reference — tedious and error-prone
SELECT * FROM orders WHERE order_date >= '&start_date';
SELECT COUNT(*) FROM orders WHERE order_date >= '&start_date';
SELECT SUM(amount) FROM orders WHERE order_date >= '&start_date';
```

```sql
-- ✅ GOOD: && defines once, reuses everywhere in the session
DEFINE start_date = TO_DATE('&_START_DATE', 'YYYY-MM-DD')

SELECT *   FROM orders WHERE order_date >= &start_date;
SELECT COUNT(*) FROM orders WHERE order_date >= &start_date;
SELECT SUM(amount) FROM orders WHERE order_date >= &start_date;
```

```sql
-- ✅ GOOD: Using the built-in _DATE and _USER substitution variables for common defaults
DEFINE run_user = UPPER('&_USER')

INSERT INTO audit_log (action, performed_by, action_date)
VALUES ('EXPORT_DATA', '&run_user', SYSDATE);
```

### Pattern 3: Escaping Literal Ampersands in Oracle SQL*Plus

When your data contains literal `&` characters, you must escape them or disable substitution to prevent unwanted prompt behavior.

```sql
-- ❌ BAD: "C++&PHP" triggers an interactive prompt for the variable named "PHP"
SET DEFINE ON
INSERT INTO language_versions (name) VALUES ('C++&PHP');
-- Result: Oracle prompts "Enter value for php:" — inserts wrong data or fails

-- ✅ GOOD A: SET ESCAPE ON with backslash before literal ampersand
SET DEFINE ON
SET ESCAPE ON
INSERT INTO language_versions (name) VALUES ('C++\&PHP');
```

```sql
-- ✅ GOOD B: SET SCAN OFF to completely disable substitution for a section
SET SCAN OFF
INSERT INTO language_versions (name) VALUES ('C++&PHP & Ruby & Go');
SET SCAN ON
-- After this, normal & substitution resumes
INSERT INTO project_tags (tag) VALUES (&project_name);
```

```sql
-- ✅ GOOD C: CONCAT function to build strings with literal ampersands without triggering substitution
SET DEFINE ON
INSERT INTO language_versions (name) VALUES (CONCAT('C++', CHR(38), 'PHP'));
-- CHR(38) produces the & character at runtime, bypassing SQL*Plus parsing entirely
```

### Pattern 4: PostgreSQL psql \set and :variable Syntax

psql uses `\set` to define variables and `:variable` (colon-prefixed) to reference them. The colon enables special modifiers like `:var::text` for type casting.

```sql
-- ❌ BAD: Hardcoded date — script cannot be reused without editing the file
SELECT * FROM events
WHERE event_date BETWEEN '2024-01-01' AND '2024-12-31'
  AND region = 'us-east';
```

```sql
-- ✅ GOOD: psql \set with shell variable interpolation for CI/CD integration
\set start_date :'START_DATE'
\set end_date :'END_DATE'
\set region :'REGION'

SELECT event_id, event_name, event_date
  FROM events
 WHERE event_date BETWEEN :'start_date'::date AND :'end_date'::date
   AND region = :'region';
```

```sql
-- ✅ GOOD B: psql \prompt for interactive use in a script
\prompt 'Enter tenant ID: ' tenant_id
\prompt 'Enter output format (csv, json, tsv): ' out_format

SELECT * FROM tenants WHERE id = :'tenant_id';
\pset format :'out_format'
```

```sql
-- ✅ GOOD C: psql shell variable passing via -v flag (CI/CD safe)
-- Command line: psql -v START_DATE="'2024-01-01'" -v END_DATE="'2024-12-31'" -v REGION=us-west -f query.sql
\set start_date :'START_DATE'
\set end_date :'END_DATE'

SELECT COUNT(*) AS event_count FROM events
 WHERE event_date BETWEEN :'start_date' AND :'end_date';
```

### Pattern 5: MySQL CLI Variable Substitution

MySQL offers multiple approaches: user-defined session variables (`@var`), shell variable expansion, and the `-e` flag with interactive substitution syntax.

```sql
-- ❌ BAD: No fallback — if @date_range is unset, comparison returns NULL silently
SET @date_range = 'monthly';

SELECT month, SUM(revenue) AS total
  FROM sales_summary
 WHERE period_type = @date_range
 GROUP BY month;
```

```sql
-- ✅ GOOD A: MySQL session variables with explicit initialization and validation
SET @table_name = 'customers';
SET @date_filter = COALESCE(NULLIF('2024-06', ''), NOW());

-- Validate table name matches expected pattern before executing dynamic SQL
SELECT CASE
  WHEN @table_name REGEXP '^[a-zA-Z_][a-zA-Z0-9_]*$' THEN 1
  ELSE SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid table name';
END;
```

```sql
-- ✅ GOOD B: Shell variable expansion in bash for MySQL CLI (CI/CD safe)
#!/usr/bin/env bash
set -euo pipefail

DB_HOST="${DB_HOST:-localhost}"
DB_NAME="${DB_NAME:-production}"
REPORT_MONTH="${REPORT_MONTH:-$(date +%Y-%m)}

mysql -h "$DB_HOST" -D "$DB_NAME" <<EOF
SET @report_month = '$REPORT_MONTH';

SELECT month, SUM(revenue) AS total_revenue, COUNT(order_id) AS order_count
  FROM sales_summary
 WHERE period_type = 'monthly'
   AND month >= @report_month
 GROUP BY month
 ORDER BY month;
EOF
```

```sql
-- ✅ GOOD C: MySQL -e with shell variable interpolation for one-liners in CI/CD
DB_PASS="$(cat /run/secrets/db_password)"
TABLE_NAME="orders_$(date +%Y%m%d)_archive"

mysql -h "$DB_HOST" \
  -u admin \
  -p"$DB_PASS" \
  -D production \
  -e "SET @archive_table = '$TABLE_NAME'; CREATE TABLE IF NOT EXISTS \$@archive_table LIKE orders;"
```

### Pattern 6: SQLite3 .variable Command for Session Variables

SQLite3 CLI provides `.variable` for defining named variables and `&varname` (in newer versions) or bound parameters for referencing them.

```sql
-- ❌ BAD: No variable — hardcoded filename makes the script unusable for other databases
.mode csv
.output /tmp/report.csv
SELECT * FROM analytics;
```

```sql
-- ✅ GOOD A: SQLite3 .variable with .read and -cmd flag
-- Command line: sqlite3 -cmd ".variable output_file" data.db < query.sql
.variable output_file TEXT :output_file

.mode csv
.output &output_file
SELECT * FROM analytics WHERE created_at >= date('now', '-30 days');

-- ✅ GOOD B: SQLite3 bound parameters via shell variable injection
#!/usr/bin/env bash
set -euo pipefail

OUTPUT_DIR="${1:-/tmp}"
TABLE_NAME="${2:-customers}"

sqlite3 data.db "
  CREATE TABLE IF NOT EXISTS \"${TABLE_NAME}_backup\" AS SELECT * FROM \"${TABLE_NAME}\";
"
cp data.db "${OUTPUT_DIR}/${TABLE_NAME}_$(date +%Y%m%d).db"
```

### Pattern 7: Handling Missing/Undefined Variables with COALESCE and NULLIF

Protecting scripts against undefined variables is critical — a missing variable should produce a sensible default, not a prompt or error.

```sql
-- ❌ BAD: Undefined &threshold triggers interactive prompt; pipeline hangs forever
SELECT * FROM anomalies
 WHERE deviation_pct > &threshold;
```

```sql
-- ✅ GOOD A: Oracle COALESCE pattern for safe defaults
DEFINE threshold = COALESCE('&threshold', '5.0')

SELECT * FROM anomalies
 WHERE deviation_pct > &threshold;

-- ✅ GOOD B: Oracle NULLIF pattern to convert empty string to NULL, then COALESCE
DEFINE date_filter = TO_DATE(COALESCE(NULLIF('&date_filter', ''), '2024-01-01'), 'YYYY-MM-DD')

SELECT * FROM sales WHERE sale_date >= &date_filter;
```

```sql
-- ✅ GOOD C: psql pattern with default fallback
\set threshold :'THRESHOLD'
-- If THRESHOLD env var is unset, NULLIF converts empty to NULL, COALESCE provides 5.0
SELECT * FROM anomalies
 WHERE deviation_pct > COALESCE(:threshold::float, 5.0);
```

```sql
-- ✅ GOOD D: MySQL pattern with IFNULL for session variable defaults
SET @threshold = IFNULL(NULLIF(@threshold, ''), 5.0);
SET @threshold = CAST(@threshold AS DECIMAL(10,2));

SELECT * FROM anomalies
 WHERE deviation_pct > @threshold;
```

### Pattern 8: CI/CD-Safe Non-Interactive Patterns

Automated pipelines must never hang on interactive prompts. These patterns pass variables through environment, files, or command-line flags.

```bash
# ❌ BAD: ACCEPT prompt in a script called by GitHub Actions — workflow times out after 6 minutes
cat > /tmp/query.sql <<'SQLEOF'
ACCEPT start_date PROMPT 'Enter start date (YYYY-MM-DD): '
SELECT COUNT(*) FROM events WHERE event_date >= &start_date;
SQLEOF
sqlplus user/pass@db @/tmp/query.sql
# This hangs indefinitely in CI — no user to type a date
```

```bash
# ✅ GOOD A: Oracle SQL*Plus with DEFINE pre-set from environment variable, -S silent mode
#!/usr/bin/env bash
set -euo pipefail

START_DATE="${START_DATE:-$(date +%Y-%m-01)}"
END_DATE="${END_DATE:-$(date +%Y-%m-15)}"

sqlplus -s user/pass@db <<SQLEOF
WHENEVER SQLERROR EXIT SQL.SQLCODE
DEFINE start_date = '&START_DATE'
DEFINE end_date = '&END_DATE'

SELECT COUNT(*) AS event_count FROM events
 WHERE event_date >= TO_DATE('&start_date', 'YYYY-MM-DD')
   AND event_date <  TO_DATE('&end_date', 'YYYY-MM-DD');
SQLEOF
```

```bash
# ✅ GOOD B: psql with -v flags and heredoc for multi-statement scripts
#!/usr/bin/env bash
set -euo pipefail

psql -v ON_ERROR_STOP=1 \
     -v START_DATE="${START_DATE:-2024-01-01}" \
     -v END_DATE="${END_DATE:-2024-06-30}" \
     -v REPORT_NAME="${REPORT_NAME:-monthly_summary}" \
     <<'PSQLEOF'
\set report_name :'REPORT_NAME'

CREATE TABLE IF NOT EXISTS reports.:\report_name AS
SELECT region, COUNT(*) AS order_count, SUM(amount) AS total_amount
  FROM orders
 WHERE order_date BETWEEN :'START_DATE'::date AND :'END_DATE'::date
 GROUP BY region;

-- Log completion for CI/CD visibility
INSERT INTO reports.execution_log (report_name, completed_at, row_count)
SELECT :'report_name', CURRENT_TIMESTAMP, COUNT(*) FROM :.report_name;
PSQLEOF
```

```bash
# ✅ GOOD C: SQLite3 with environment variables and output redirection
#!/usr/bin/env bash
set -euo pipefail

DB_PATH="${DB_PATH:-/data/analytics.db}"
OUTPUT_CSV="${OUTPUT_CSV:-/tmp/report_$(date +%Y%m%d).csv}"
TABLE_FILTER="${TABLE_FILTER:-analytics_2024*}"

sqlite3 "$DB_PATH" <<SQL
.mode csv
.headers on
.output $OUTPUT_CSV
SELECT * FROM analytics WHERE category LIKE '${TABLE_FILTER}' ORDER BY created_at DESC;
SQL

echo "Report written to: $OUTPUT_CSV ($(wc -l < "$OUTPUT_CSV") lines)"
```

### Pattern 9: Shell-Based Parameter Passing with SQL Scripts

Combine shell scripting (variables, validation, loops) with database-specific substitution for powerful automation.

```bash
# ❌ BAD: Hardcoded table name in every invocation — no parameterization
#!/bin/bash
sqlite3 /data/sales.db "SELECT COUNT(*) FROM sales_2024;"
```

```bash
# ✅ GOOD A: Shell loop over multiple tables with Oracle SQL*Plus
#!/usr/bin/env bash
set -euo pipefail

readonly DB_CONN="user/password@orclpdb"
readonly TABLES=("customers" "orders" "inventory" "payments")

for tbl in "${TABLES[@]}"; do
  echo "--- Processing: ${tbl} ---"

  sqlplus -s "$DB_CONN" <<SQLEOF
SET PAGESIZE 50
SET FEEDBACK ON
DEFINE table_name = '${tbl}'

SELECT COUNT(*) AS row_count,
       MAX(created_at) AS latest_entry
  FROM &table_name;
SQLEOF
done
```

```bash
# ✅ GOOD B: PostgreSQL with -v flags in a cron-usable script
#!/usr/bin/env bash
set -euo pipefail

readonly TARGET_DB="${1:?Usage: $0 <database_name>}"
readonly MAX_ROWS="${2:-1000}"

psql -t -A -v ON_ERROR_STOP=1 \
     -v TARGET_DB="$TARGET_DB" \
     -v MAX_ROWS="$MAX_ROWS" \
     -c "
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(quote_ident(:'TARGET_DB')::regclass)) AS size
  FROM pg_tables
 WHERE tablename = :'TARGET_DB';

SELECT COUNT(*) FROM :'TARGET_DB';
"
```

---

## Constraints

### MUST DO
- Always provide a default or fallback value for every substitution variable using `COALESCE`, `IFNULL`, or explicit `DEFINE` defaults
- Use `SET SCAN OFF` / `SET DEFINE OFF` when your data contains literal ampersands that must not trigger substitution
- Run SQL*Plus scripts in `-S` (silent) mode in automated environments to suppress banner output
- Validate variable content before injection — especially for table names or schema qualifiers used in dynamic SQL
- Use `&&varname` instead of `&varname` when the same value is referenced multiple times to avoid redundant prompts
- Wrap date/time substitutions in `TO_DATE()` / `::date` casts with explicit format masks
- Test every script with the variable intentionally unset to confirm the default activates cleanly

### MUST NOT DO
- Never use interactive `ACCEPT` or `\prompt` commands in CI/CD pipelines, cron jobs, or any non-interactive context — they will hang the pipeline
- Do not concatenate user input directly into SQL strings without validation — this enables injection attacks even through CLI substitution
- Do not rely on Oracle's default prompt text "Enter value for X:" as documentation — always provide custom `PROMPT '...'` messages
- Do not omit `WHENEVER SQLERROR EXIT` in production scripts — errors should terminate the script rather than silently continue
- Do not use bare `&variable` references in MySQL without ensuring the variable is set via `SET @var = ...` first

---

## Output Template

When applying this skill, produce:

1. **Dialect identification** — State which database CLI tool is targeted (Oracle SQL*Plus, PostgreSQL psql, MySQL CLI, SQLite3)
2. **Variable definition block** — Show the exact command to define each variable with its default/fallback value
3. **Substitution references** — Show how each variable is referenced in the SQL using the correct syntax for that dialect
4. **Escape mechanism** — If literal ampersands appear in data, show the escaping approach (SET ESCAPE, CONCAT + CHR(38), SET SCAN OFF)
5. **Missing-variable guard** — Wrap every substitution in `COALESCE()` or provide an explicit default via `DEFINE`/`\set`
6. **CI/CD wrapper script** — For automated execution, provide the full bash wrapper with environment variable sourcing and non-interactive flags

---

## Related Skills

| Skill | Purpose |
|---|---|
| `linux-shell-command-chaining` | Shell-based parameter passing to SQL scripts via environment variables, heredocs, and command-line argument parsing |
| `error-handling` | Missing variable error recovery — detecting undefined variables at runtime and producing structured error messages instead of hangs |

---

## Live References

> Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references.

- [Oracle SQL*Plus User's Guide — Substitution Variables](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlpl/substitution-variables.html)
- [Oracle SQL*Plus DEFINE and ACCEPT Commands Reference](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlpl/accept.html)
- [PostgreSQL psql Documentation — Meta-commands and Variables](https://www.postgresql.org/docs/current/app-psql.html#APP-PSQL-META-COMMAND-VARIABLES)
- [MySQL Command-Line Client Documentation](https://dev.mysql.com/doc/refman/8.4/en/mysql.html)
- [SQLite3 Command Line Shell — Variables and I/O Redirection](https://www.sqlite.org/cli.html)
- [Oracle SQL*Plus SET Commands — ESCAPE, DEFINE, SCAN](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlpl/set.html)
- [psql Variable Substitution Modifiers (colon-prefixed :var modifiers)](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.