
Product
Socket Threat Intel Now Shows Which Malware Campaigns Were Blocked or Detected
Socket Threat Intel now shows which malware campaigns were blocked or detected in your organization, with links to scans and Firewall events.
@teamshift/workflow-lint
Advanced tools
Preflight linter for AI-agent and automation workflows (n8n exports and a generic YAML/JSON spec): catches missing error handling, unsafe retries, unapproved money/delete/send actions, missing timeouts, hardcoded secrets and unverified webhooks before the
workflow-lint is an open-source preflight linter that catches missing error handling, unsafe retries, unapproved money actions and hardcoded secrets in AI-agent and automation workflows before they run.
It reads n8n workflow exports and a small, tool-agnostic YAML/JSON workflow spec, applies nine rules drawn from real automation failures (double-sent emails, double charges, runs that "succeeded" while doing nothing), and prints a terminal report, JSON, or SARIF for CI. The core is pure TypeScript with no Node.js built-ins, so the same engine runs in the browser.
Try it in your browser: teamshift.io/tools/workflow-lint, no login, and nothing leaves the page.
npx @teamshift/workflow-lint my-workflow.json # n8n export
npx @teamshift/workflow-lint agent.workflow.yaml # generic spec
npx @teamshift/workflow-lint flows/*.json --format sarif > workflow-lint.sarif
Exit code 0 = no errors, 1 = at least one error-severity finding, 2 = unreadable input or a usage error.
Running it on the n8n fixture test/fixtures/n8n-bad.json prints the following (fix/docs lines trimmed):
test/fixtures/n8n-bad.json (n8n, 7 steps, workflow "New order follow-up")
error WL008 unverified-webhook line 7
Webhook trigger "Order Webhook" does not verify a signature (authentication: none and no HMAC/Crypto verification node downstream).
error WL006 hardcoded-secret line 20
Step "Fetch Orders" contains a hardcoded Bearer token: FAKE…89 (34 chars).
error WL001 missing-error-handling line 55
Step "Email Customer" (send) has no error handling; a failure will stop the run with no alert or fallback.
error WL002 unsafe-retry line 55
Step "Email Customer" retries up to 3 times but has no idempotency key; a retry after a timeout can repeat the send.
error WL004 unapproved-high-risk-action line 55
Step "Email Customer" sends to external recipients without a human approval gate.
error WL001 missing-error-handling line 65
Step "Charge Deposit" (money) continues on error, so a failed payment is silently treated as success.
error WL004 unapproved-high-risk-action line 65
Step "Charge Deposit" moves money without a human approval gate.
warning WL005 no-completion-check $
Workflow "New order follow-up" has 2 side-effect step(s) but no completion check; it can "succeed" without verifying the end state.
warning WL003 missing-timeout line 20
Step "Fetch Orders" calls an external system with no timeout.
warning WL007 unbounded-loop line 20
Step "Fetch Orders" loops/paginates with no upper bound (pagination without limitPagesFetched/maxRequests).
warning WL007 unbounded-loop line 28
Execution cycle "Poll Export" → "Export Ready?" → "Poll Export" has no loop limit.
warning WL009 pii-to-llm-without-note line 47
LLM step "Draft Thank-You" receives personal data (first_name, last_name, email, phone) with no PII handling note.
✖ 12 problems (7 errors, 5 warnings)
The fixed version, test/fixtures/n8n-good.json, reports ✓ 0 problems.
| ID | Name | Severity | Catches |
|---|---|---|---|
| WL001 | missing-error-handling | error | Side-effect step with no error path, or one that silently continues on error |
| WL002 | unsafe-retry | error | Retried write/send/payment with no idempotency key |
| WL003 | missing-timeout | warning | External call or wait with no timeout |
| WL004 | unapproved-high-risk-action | error | Money, delete, or external send with no human approval gate |
| WL005 | no-completion-check | warning | Workflow never verifies the end state |
| WL006 | hardcoded-secret | error | API keys, tokens, signing secrets, private keys in parameters |
| WL007 | unbounded-loop | warning | Loops, pagination or execution cycles without a limit |
| WL008 | unverified-webhook | error | Webhook trigger that does not verify a signature or authenticate |
| WL009 | pii-to-llm-without-note | warning | Personal data sent to an LLM step without a documented handling note |
A step that writes, sends, moves money or deletes must say what happens when it fails. Generic: set onError (stop, alert, fallback:<step>, ...). onError: continue / ignore is reported as swallowing errors. n8n: node setting On Error → Continue (using error output) with the error output wired up, or a workflow-level Error Workflow. continueOnFail / Continue (regular output) on a side-effect node is reported.
Retries plus a non-idempotent side effect means a timeout followed by a retry can send twice or charge twice. Add an idempotencyKey (generic), or send an Idempotency-Key header from the n8n HTTP Request node. You can also turn retries off for that step.
HTTP/API calls, LLM calls, payments, messaging steps, approval steps and open-ended waits need timeoutSec. In n8n, this rule checks the HTTP Request Timeout option and the Wait node's Limit Wait Time when it resumes on a webhook or form (a human approval that nobody answers blocks the run forever).
Steps with sideEffect: money or delete, or send to external recipients, need human approval. Generic: approval: { required: true, by: <role> } on the step, or an earlier approval.* step. n8n: an upstream Send and Wait for Response operation (Gmail, Slack, ...), a Form node, or a Wait node that resumes on a webhook or form. Messages to internal chat (Slack, Teams) don't require approval unless marked audience: external.
An automation that sends, writes or charges should confirm the result instead of assuming success. Generic: a workflow-level completionCheck, a step-level completionCheck, or a verify.* step. n8n (heuristic): downstream of a side-effect node there is a read-back (an HTTP GET, or a get/getAll/search/read operation), or a side-effect-free node named like Verify…, Confirm…, Check…. Set/NoOp/IF nodes on their own don't count.
Scans every parameter for Stripe, OpenAI, Anthropic, AWS, GitHub, Slack, Google, Shopify, Twilio, SendGrid and HubSpot key shapes, plus private key blocks, JWTs, literal Bearer and Basic credentials, and literal values under keys named apiKey, secretKey, accessKey, token, secret, password or authorization. Expressions ({{ $env.X }}, ={{ ... }}, ${VAR}) are ignored. Matches are redacted in the output.
Generic: loop.* steps or paginate: blocks need limit, maxPages, maxIterations or maxItems. n8n: HTTP Request pagination without Limit Pages Fetched, and any connection cycle that doesn't pass through Loop Over Items (Split In Batches).
Generic: trigger: { type: webhook, verifySignature: true } (or auth: hmac). n8n: Webhook node Authentication set to Basic, Header or JWT, or a Crypto (HMAC) or Code node downstream that verifies the signature (createHmac, timingSafeEqual, constructEvent, ...). To check the signature itself, use webhook-inspect.
Flags LLM/agent steps whose inputs reference emails, phone numbers, names, addresses, dates of birth, card numbers and similar fields. The check looks inside {{ }} expressions, generic inputs keys and explicit pii: [...] lists. Fix it by minimizing the fields and adding piiNote: (generic), or by adding a node Note in n8n that mentions PII, privacy, redaction or retention.
Use it for agents and automations built with code, Make, Zapier, Temporal, LangGraph or anything else: describe the steps, and workflow-lint checks the safety properties.
name: Overdue invoice chaser
trigger:
type: webhook # webhook | schedule | manual | event
verifySignature: true # webhook only
completionCheck: # how the workflow proves it worked
description: Every reminded invoice has reminder_sent_at set.
lint:
ignore: [WL009] # optional per-file rule suppression
steps:
- id: send-reminder # required, unique
action: email.send # <system-or-kind>.<verb>; kind and sideEffect are inferred from it
system: gmail
sideEffect: send # none | write | send | money | delete
audience: external # external | internal (for send; email defaults to external, chat to internal)
onError: alert # stop | alert | retry | fallback:<id> | continue (= swallowed)
retry: { maxAttempts: 3 }
timeoutSec: 30
idempotencyKey: "{{ invoice.id }}-reminder-1"
approval: { required: true, by: office-manager }
completionCheck: Gmail returns a message id
inputs: { to: "{{ invoice.customer.email }}" }
piiNote: Customer email is required to send the reminder.
| Field | Meaning |
|---|---|
action | <prefix>.<verb>. Prefixes such as http, email, slack, llm, wait, loop, stripe, crm, approval, verify set the step kind. Verbs such as send, charge, refund, delete, create and update set the default sideEffect |
sideEffect | Explicit side effect; overrides inference |
onError, retry, timeoutSec, idempotencyKey, approval, completionCheck | Inputs to the rules above |
loop / paginate | { limit | maxPages | maxIterations | maxItems } |
durationSec | Fixed delay for wait.* steps (no timeout needed) |
pii, piiNote | Declared personal-data fields and the handling note |
params / inputs | Scanned for secrets and PII references |
import { lint, formatPretty } from "@teamshift/workflow-lint";
const result = lint(workflowText, { filename: "flow.json", ignore: ["WL005"] });
if (result.summary.errors > 0) console.log(formatPretty([result]));
lint() is synchronous and pure. It takes JSON or YAML text and returns { findings, summary, format, stepCount }. formatSarif() produces SARIF 2.1.0 for GitHub code scanning and other CI tools.
Export it (Workflow → Download, or copy all nodes) and run npx @teamshift/workflow-lint workflow.json. Pasted node selections work too, because workflow-lint only needs nodes and connections.
Give every side-effect step an idempotency key derived from the business object, for example {{invoice.id}}-reminder-2, and pass it to the provider: an Idempotency-Key header for Stripe, or a dedupe check before sending email. WL002 flags every retried side effect that lacks one.
Anything that moves money, deletes data, or sends to customers, vendors or the public, at least until the automation has a track record. WL004 enforces this. To work out the matching OAuth scopes and approval policy, see permission-planner.
Run workflow-lint on the export. WL006 recognizes common provider key formats and literal values under secret-named fields, and it redacts the value in its output. Move the key to n8n Credentials or an environment variable, then rotate it.
A step that reads back the end state, such as "the invoice status is now sent" or "the row exists", instead of trusting that each API call returned 200. Without one, a workflow can report success after doing nothing. WL005 flags workflows with side effects and no completion check.
Yes. Use --format sarif for GitHub code scanning and similar tools, or --format json for custom gates. The exit code is non-zero when there are error-severity findings.
No. The CLI is local, and the browser version runs the same pure core client-side.
Built by TeamShift — AI workers for small-business operations.
FAQs
Preflight linter for AI-agent and automation workflows (n8n exports and a generic YAML/JSON spec): catches missing error handling, unsafe retries, unapproved money/delete/send actions, missing timeouts, hardcoded secrets and unverified webhooks before the
The npm package @teamshift/workflow-lint receives a total of 2 weekly downloads. As such, @teamshift/workflow-lint popularity was classified as not popular.
We found that @teamshift/workflow-lint demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Product
Socket Threat Intel now shows which malware campaigns were blocked or detected in your organization, with links to scans and Firewall events.

Security News
Fixes for eMudhra and Brotli are out. Getting them everywhere they’re needed means tracking down bundled libraries, runtimes, and certificate stores.

Threat Research
/Security News
Socket uncovered secret-stealing workflows that ran in LiteLLM’s GitHub repository 11 days before the supply chain attack on PyPI and Docker.