security-lint

A governed artifact in the stable bundle. Replacing this file on your machine makes your next attestation come back Drifted, naming this path.

DescriptionRun the severity-tiered security linter over the codebase before committing. Checks SEC-CRITICAL and SEC-HIGH (block the commit), SEC-MEDIUM (warn), and SEC-COST (block the deploy). Run before every non-trivial commit — this is the adversarial gate.
Arguments[file-or-directory] [--verbose]
Tools it may use[Bash, Read, Grep]
Installs atskills/security-lint/SKILL.md
Mode0644 — never executable
Size8366 bytes
SHA-256259a2ef59c2eb22199fe3241c0c20ae3c5f3c5b84bb1005f31c21415d5a3c716
fetch the raw bytes

The complete file, verbatim

This is the entire SKILL.md, including its YAML frontmatter: byte-identical to what installs, not rendered and not reformatted. Hash exactly what is below and you get the digest above. The table is only a reading aid; this block is the artifact.

---
name: security-lint
description: Run the severity-tiered security linter over the codebase before committing. Checks SEC-CRITICAL and SEC-HIGH (block the commit), SEC-MEDIUM (warn), and SEC-COST (block the deploy). Run before every non-trivial commit — this is the adversarial gate.
argument-hint: "[file-or-directory] [--verbose]"
allowed-tools: [Bash, Read, Grep]
---

# security-lint — the adversarial gate

A mistwire practice.

You have been invoked to run the security lint pass. This is a **gate**, not a
suggestion: it runs before every non-trivial commit, and a red SEC-CRITICAL or
SEC-HIGH blocks the commit until it is fixed or explicitly suppressed.

The value here is not any one pattern in the rule set. It is that the scan is
**cheap, tiered, and mandatory** — so it actually runs, and so a finding carries
an unambiguous consequence instead of a debate.

---

## 1. Where the linter lives

The linter is a **test in the ordinary test suite**, not a separate tool with its
own lifecycle. That placement is deliberate and worth copying:

- It runs wherever the suite already runs — CI included, on every branch, with no
  extra wiring.
- A new violation fails the build like any other test.
- Nobody has to remember to install anything.

Ours is a single Python module under `tests/unit/` that walks the source tree and
asserts on pattern matches. Any equivalent works — a custom rule file for your
existing linter, a `go vet` analyzer, an ESLint plugin. What matters is the four
properties below, in section 3.

---

## 2. Execute

> **Precondition:** the first command below assumes a security-lint test module
> wired into your project's test runner. Substitute your own invocation; the
> shape is the pattern, not a portable command. The scoped grep in the second
> block needs nothing but `grep`.

### Full scan (no arguments)

Run the linter across the whole source tree:

```bash
# Pattern — adapt the runner and path to your project.
pytest tests/unit/test_security_lint.py -v --tb=short
```

Do not pipe that output through `head` or `tail`. A truncated failure list is
indistinguishable from a shorter one, and a security gate is the last place you
can afford that ambiguity. If the output is long, redirect it to a file and read
the file. `--verbose` maps to your runner's verbosity flag; it changes the
reporting detail, never the rule set.

### Scoped scan (a file or directory was named)

Most linters of this kind scan the full tree by default and have no meaningful
scoped mode. Rather than pretend otherwise, fall back to a direct pattern grep
over the target — substitute the real path for the placeholder:

```bash
grep -rnE "import random|uuid\.uuid4|eval\(|exec\(|compile\(|pickle\.load|shell=True" path/to/file-or-dir
```

That expression is the concrete, adoptable core of the rule set:

| Pattern | Why it is a finding |
|---|---|
| `import random`, `uuid.uuid4` | Non-cryptographic randomness reaching security-relevant code (tokens, nonces, session ids). Use the CSPRNG. |
| `eval(`, `exec(`, `compile(` | Dynamic code execution — arbitrary-code-execution surface, usually avoidable. |
| `pickle.load` | Deserialization of untrusted data is remote code execution. |
| `shell=True` | Command injection surface in subprocess calls. |

A grep is a blunt instrument: it will flag `import random` in a benchmark script
and miss an aliased import entirely. That is the trade — it costs nothing to run
and needs no install, so it is what you reach for on a single file. The test-suite
linter is what you trust across the tree.

Add your own **banned-dependency** patterns to the same expression. We ban a
specific set of third-party agent/LLM frameworks from the source tree for supply
chain and governance reasons; yours will differ. The mechanism generalizes even
though our list does not.

---

## 3. The four properties that make this work

If you copy nothing else, copy these:

1. **Stable rule IDs.** Every rule has a permanent identifier (`C01`, `H03`,
   `M02`, `COST04`). IDs never get reused or renumbered. They are what
   suppressions cite, what commit messages reference, and what makes
   "we accepted this in March" auditable a year later.
2. **Severity tiers with distinct consequences.** A tier that does not change
   anyone's behavior is decoration.
3. **Explicit, greppable suppressions.** No silent exceptions.
4. **It runs in the normal test suite.** See section 1.

### Severity tiers

| Tier | Rule IDs | Consequence |
|---|---|---|
| SEC-CRITICAL | `C01`–`C07` | **Blocks the commit** |
| SEC-HIGH | `H01`–`H07` | **Blocks the commit** |
| SEC-MEDIUM | `M01`–`M02` | Warn — fix or file it |
| SEC-COST | `COST01`–`COST04` | **Blocks the deploy** (not the commit) |

The cost tier is worth calling out. Runaway spend on metered infrastructure —
unbounded model calls, missing budget caps, absent retry ceilings — is a
production-severity failure that no conventional security linter looks for. It
blocks at a different boundary than the security tiers do: bad code can land on
a branch, but it cannot reach an environment that spends money.

Maintain a rule catalog in-repo mapping each ID to its rule, rationale, and
remediation. The IDs are load-bearing only if someone can look them up.

---

## 4. Suppressions

A violation may be suppressed inline with a comment naming the exact rule. Cite
the **rule ID** (`C03`), not the tier name (`SEC-CRITICAL`) — the tier tells a
reader how much it hurts, the ID tells them what was actually waived:

```python
token = random.random()  # nosec: C03 — non-security demo fixture; rationale in <your decision record>
```

Rules for suppressions:

- The rule ID is **mandatory**. A bare blanket-suppress comment suppresses
  everything forever, including the rule nobody has written yet.
- A justification is **mandatory**. "Reviewed" is not a justification. Point at a
  durable record — a decision log entry, an issue, an ADR — not at a person's
  memory.
- Suppressions are valid, but they are **debt**. Sweep them periodically — grep
  for the suppression marker and re-litigate each one. A suppression whose
  author has left and whose rationale nobody can reconstruct is a finding.

---

## 5. Report

- **All pass:** "Security lint clean. Clear to commit."
- **Failures:** list every violation with **rule ID, file, line number, and the
  fix**. Then state plainly: "Commit BLOCKED until SEC-CRITICAL and SEC-HIGH
  violations are resolved." Do not soften this and do not bury it under a
  summary of what passed.
- **Suppressed:** note each suppression encountered, with its rule ID. They are
  valid, and they are also the thing most likely to be hiding the real problem.
- **Not run:** if the linter could not run — missing dependency, broken
  environment, scoped-out path — report **NOT-RUN** explicitly. A check that did
  not run is never a pass, and never a silent skip. This applies per tier: if the
  scoped grep covered one file, the tree was **not** scanned, and the report says
  so rather than implying clean coverage.

---

## 6. The adversarial mindset

The linter catches patterns. It does not catch design. When reviewing anything
non-trivial, walk this list deliberately — it is the class-of-attack checklist
that pattern matching structurally cannot reach:

- **Crypto bypass** — is there a path that reaches the protected resource
  without passing the check at all?
- **Bounds** — every index, every length, every allocation sized from input.
- **Epoch replay** — can a valid-but-old message, token, or nonce be replayed
  after rotation?
- **Ordering** — what breaks if these operations arrive out of order, or
  concurrently?
- **TOCTOU** — what changes between the check and the use?
- **Input trust** — where exactly does untrusted data become trusted, and is
  that boundary a single, named, testable place?
- **Collision** — can two distinct inputs produce the same identifier, key, or
  path?
- **Rollback** — does downgrading to a previous version re-open something this
  version closed?

---

## 7. Pairs with

- **`verify-ladder`** — this skill is rung 6 of that ladder. The ladder is how
  you make sure rung 6 is not the rung that quietly gets skipped.
- **Your commit ritual** — the gate runs before staging, and its result is
  reported, not assumed.

All skills · Back to the overview