Self-Hosted Git PR Diff Reviewer with n8n & Qwen2.5-Coder: Secret Redaction & Token Caps
Automate pull request bug and security reviews on your own GPU using n8n and Qwen2.5-Coder-14B—complete with lockfile filtering, regex secret scrubbing, and scoped webhook tokens.
Self-hosted n8n + Local Ollama (`qwen2.5-coder:14b` on 16GB+ GPU / 24GB+ Mac)
When paired with self-hosted GitLab/Gitea, the entire PR review loop operates inside your air-gapped intranet.
- Raw PR diffs that include `package-lock.json`, `pnpm-lock.yaml`, or minified bundles easily exceed 60K tokens; filter out lockfiles and cap diffs to 12,000 characters per hunk.
- Developers occasionally commit real `.env` secrets in a PR commit; always run a deterministic regex redaction pass before logging or forwarding diffs.
1. Pre-Filtering Lockfiles & Scrubbing Secrets Before LLM Review
| Security / Ops Control | Unsafe Default | Tinker Verified Standard | Why It Matters |
|---|---|---|---|
| Git API Token Scope | Personal Admin PAT (Full repo delete/push) | Fine-grained: Pull Requests (Read & Comment only) | Prevents unauthorized code push if n8n is compromised |
| Diff Payload Size | Unbounded full PR diff | Exclude lockfiles + slice(0, 12000) | Keeps review time under 15s and prevents KV OOM |
| Secret Leak Handling | Send raw secret into LLM prompt & logs | Regex mask `[REDACTED_SECRET]` + instant alert | Stops secrets from persisting in n8n execution history |
const rawDiff = String($input.first().json.diff || '');
const cleanHunks = rawDiff
.split(/^diff --git /m)
.filter(h => h && !/(package-lock\.json|pnpm-lock\.yaml|\.min\.js|dist\/)/.test(h.split('\n')[0]))
.join('diff --git ');
const secretRegex = /(sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{30,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]+ PRIVATE KEY-----)/g;
const hadSecretLeak = secretRegex.test(cleanHunks);
const safeDiff = cleanHunks.replace(secretRegex, '[REDACTED_SECRET]').slice(0, 12000);
return [{ json: { safeDiff, hadSecretLeak } }];2. Enforcing High-Signal JSON Output (`severity`, `file`, `line`, `fix`)
3. Filtering Webhook Events (`opened` / `synchronize`) & Skipping Draft PRs
Frequently Asked Questions & Edge Cases
Q: Is `qwen2.5-coder:7b` enough for PR review, or should I use `14b` / `32b`?
`qwen2.5-coder:7b` catches syntax errors and obvious missing `await` calls quickly, but `qwen2.5-coder:14b` (or `32b`) has a significantly lower false-positive rate on multi-function logic and edge-case null checks. Since PR reviews run asynchronously in the background, waiting 12 seconds for `14b` is well worth the accuracy gain.
Q: What happens if a developer opens a huge PR touching 45 files?
Split the diff array per file in an n8n `Split Out` node, prioritize security-sensitive paths (`src/auth/*`, `src/api/*`, `migrations/*`), and review top-priority files in sequential 4,000-character batches rather than stuffing all 45 files into a single prompt.
n8n + Local Qwen2.5-Coder Git PR Diff Reviewer with Secret Redaction
Scrubs accidental API keys/tokens from git diffs before sending code hunks to local Qwen2.5-Coder-14B for bug and security review.
{
"name": "Tinker AI — Local Git Diff Security & Bug Reviewer",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "git-pr-review",
"responseMode": "lastNode"
},
"name": "PR Diff Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [200, 300]
},
{
"parameters": {
"jsCode": "const rawDiff = String($input.first().json.body?.diff || '').slice(0, 12000);\nconst sanitizedDiff = rawDiff.replace(/(sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16})/g, '[REDACTED_SECRET]');\nreturn [{ json: { sanitizedDiff } }];"
},
"name": "Sanitize Secrets & Cap Diff Length",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [440, 300]
},
{
"parameters": {
"method": "POST",
"url": "http://host.docker.internal:11434/api/generate",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'qwen2.5-coder:14b', format: 'json', stream: false, options: { temperature: 0.1, num_ctx: 16384 }, prompt: 'Review this git diff for security bugs, race conditions, or unhandled errors. Return JSON {summary, issues: [{severity, lineHint, suggestion}]}: \n' + $json.sanitizedDiff }) }}",
"options": { "timeout": 60000 }
},
"name": "Qwen2.5-Coder Diff Review",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 2,
"continueOnFail": true,
"position": [680, 300]
}
],
"connections": {
"PR Diff Webhook": {
"main": [[{ "node": "Sanitize Secrets & Cap Diff Length", "type": "main", "index": 0 }]]
},
"Sanitize Secrets & Cap Diff Length": {
"main": [[{ "node": "Qwen2.5-Coder Diff Review", "type": "main", "index": 0 }]]
}
}
}