loki-guardrails 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. loki_guardrails-0.1.0/.gitignore +24 -0
  2. loki_guardrails-0.1.0/LICENSE +24 -0
  3. loki_guardrails-0.1.0/PKG-INFO +237 -0
  4. loki_guardrails-0.1.0/README.md +214 -0
  5. loki_guardrails-0.1.0/benchmarks/README.md +161 -0
  6. loki_guardrails-0.1.0/loki.py +8017 -0
  7. loki_guardrails-0.1.0/packaging/__init__.py +5 -0
  8. loki_guardrails-0.1.0/pyproject.toml +99 -0
  9. loki_guardrails-0.1.0/shim/loki-omp.ts +32 -0
  10. loki_guardrails-0.1.0/shim/loki.ts +101 -0
  11. loki_guardrails-0.1.0/templates/.credo.exs +29 -0
  12. loki_guardrails-0.1.0/templates/.golangci.yml +38 -0
  13. loki_guardrails-0.1.0/templates/.oxlintrc.json +66 -0
  14. loki_guardrails-0.1.0/templates/.ruff.toml +13 -0
  15. loki_guardrails-0.1.0/templates/claude-settings.fragment.json +39 -0
  16. loki_guardrails-0.1.0/templates/codex-hooks.json +40 -0
  17. loki_guardrails-0.1.0/templates/factory-hooks.json +37 -0
  18. loki_guardrails-0.1.0/templates/loki.json +12 -0
  19. loki_guardrails-0.1.0/templates/loki.yml +70 -0
  20. loki_guardrails-0.1.0/tests/__init__.py +21 -0
  21. loki_guardrails-0.1.0/tests/helpers.py +36 -0
  22. loki_guardrails-0.1.0/tests/loki-omp.test.ts +22 -0
  23. loki_guardrails-0.1.0/tests/loki-shim.test.ts +188 -0
  24. loki_guardrails-0.1.0/tests/test_admission.py +122 -0
  25. loki_guardrails-0.1.0/tests/test_analyzers.py +872 -0
  26. loki_guardrails-0.1.0/tests/test_benchmark_contracts.py +51 -0
  27. loki_guardrails-0.1.0/tests/test_cli.py +72 -0
  28. loki_guardrails-0.1.0/tests/test_competitive_features.py +206 -0
  29. loki_guardrails-0.1.0/tests/test_comprehensive_benchmark.py +336 -0
  30. loki_guardrails-0.1.0/tests/test_content_rules.py +905 -0
  31. loki_guardrails-0.1.0/tests/test_daemon.py +141 -0
  32. loki_guardrails-0.1.0/tests/test_dependencies.py +217 -0
  33. loki_guardrails-0.1.0/tests/test_elixir_analysis.py +145 -0
  34. loki_guardrails-0.1.0/tests/test_installed_hooks.py +145 -0
  35. loki_guardrails-0.1.0/tests/test_language_commands.py +219 -0
  36. loki_guardrails-0.1.0/tests/test_language_rulesets.py +145 -0
  37. loki_guardrails-0.1.0/tests/test_prewrite_content.py +334 -0
  38. loki_guardrails-0.1.0/tests/test_python_ast.py +177 -0
  39. loki_guardrails-0.1.0/tests/test_repository_guardrails.py +374 -0
  40. loki_guardrails-0.1.0/tests/test_scan_installer.py +291 -0
  41. loki_guardrails-0.1.0/tests/test_shell_permissions.py +264 -0
  42. loki_guardrails-0.1.0/tests/test_slop.py +397 -0
  43. loki_guardrails-0.1.0/tests/test_sobelow_delta.py +230 -0
  44. loki_guardrails-0.1.0/tests/test_utilities.py +205 -0
  45. loki_guardrails-0.1.0/vendor/anti-slop/LICENSE +21 -0
  46. loki_guardrails-0.1.0/vendor/anti-slop/index.ts +41 -0
  47. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-chained-type-assertions.ts +77 -0
  48. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
  49. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-known-value-widening.ts +247 -0
  50. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-module-mocking.ts +91 -0
  51. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-object-parameters.ts +126 -0
  52. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-reflect-apply.ts +28 -0
  53. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-reflect-get.ts +28 -0
  54. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-runtime-typeof.ts +67 -0
  55. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-shape-in-symbol-names.ts +39 -0
  56. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-unknown-parameters.ts +83 -0
  57. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-unknown-returns.ts +115 -0
  58. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-unknown-type-aliases.ts +70 -0
  59. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-unsafe-dictionary-type.ts +134 -0
  60. loki_guardrails-0.1.0/vendor/anti-slop/rules/no-widen-then-assert.ts +366 -0
  61. loki_guardrails-0.1.0/vendor/anti-slop/rules/require-safety-comment-for-type-assertion.ts +62 -0
  62. loki_guardrails-0.1.0/vendor/anti-slop/shared/dictionary-types.ts +502 -0
  63. loki_guardrails-0.1.0/vendor/anti-slop/shared/lexical-type-parameters.ts +61 -0
  64. loki_guardrails-0.1.0/vendor/anti-slop/shared/reflect-method.ts +35 -0
@@ -0,0 +1,24 @@
1
+ .coverage
2
+ .mutmut-cache/
3
+ .pytest_cache/
4
+ .venv/
5
+ mutants/
6
+ __pycache__/
7
+ .ruff_cache/
8
+ node_modules/
9
+
10
+ # Local agent installations, session state, and working notes.
11
+ /.agents/
12
+ /.claude/
13
+ /.completion-loop/
14
+ /handoff.md
15
+ /research.md
16
+ /research-addendum.md
17
+ /skills-lock.json
18
+ /artifacts/tmp/
19
+
20
+ # Local credentials.
21
+ .env
22
+ .env.*
23
+ !.env.example
24
+ dist/
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bryan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ The vendored anti-slop Oxlint plugin in vendor/anti-slop is MIT licensed by
24
+ Dillon Mulroy; see vendor/anti-slop/LICENSE.
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.5
2
+ Name: loki-guardrails
3
+ Version: 0.1.0
4
+ Summary: Deterministic guardrails for AI coding agents: blocks unsafe edits before they land
5
+ Project-URL: Homepage, https://github.com/alfredosdpiii/loki
6
+ Project-URL: Documentation, https://github.com/alfredosdpiii/loki/blob/main/docs/reference.md
7
+ Project-URL: Issues, https://github.com/alfredosdpiii/loki/issues
8
+ Author: Bryan
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ License-File: vendor/anti-slop/LICENSE
12
+ Keywords: ai-agents,claude-code,codex,guardrails,hooks,linting,static-analysis
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+
24
+ <div align="center">
25
+
26
+ # Loki
27
+
28
+ **Deterministic guardrails for AI coding agents.**
29
+
30
+ Loki checks every edit your agent makes with real analyzers and precise rules. It
31
+ blocks the dangerous ones before the file changes, tells the agent exactly what
32
+ to fix, and does all of this without calling a model.
33
+
34
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776ab?logo=python&logoColor=white)
35
+ ![Runtime dependencies: none](https://img.shields.io/badge/runtime%20dependencies-none-2ea44f)
36
+ ![Agents](https://img.shields.io/badge/agents-Claude%20Code%20%C2%B7%20Codex%20%C2%B7%20Factory%20%C2%B7%20Pi%20%C2%B7%20OMP-8a63d2)
37
+ ![Languages](https://img.shields.io/badge/languages-Python%20%C2%B7%20TS%2FJS%20%C2%B7%20Go%20%C2%B7%20Rust%20%C2%B7%20Elixir-e36209)
38
+
39
+ <img src="https://raw.githubusercontent.com/alfredosdpiii/loki/main/docs/demo/demo.gif" alt="Loki blocking an XSS write, a leaked API key and a cross-file type error, then allowing the fix and flagging a complexity hotspot" width="820">
40
+
41
+ <sub>Real hook output from <a href="https://github.com/alfredosdpiii/loki/blob/main/docs/demo/demo.sh">docs/demo/demo.sh</a>. Only the typing is simulated.</sub>
42
+
43
+ </div>
44
+
45
+ ## Why Loki
46
+
47
+ - **It stops problems before they land.** Content rules, Ruff and a full
48
+ TypeScript type check run on the proposed file before the write. XSS, leaked
49
+ keys and broken callers never reach disk.
50
+ - **It only complains about what the edit changed.** Findings are compared
51
+ against the committed file by rule and source line. Existing debt and shifted
52
+ lines pass, so agents aren't blocked by code they didn't touch.
53
+ - **It uses real analyzers.** mypy, `tsc`, golangci-lint, Clippy, Credo and
54
+ Sobelow run on every write, all net-new. Twenty-six tool-free rules cover
55
+ what linters miss: SSRF, open redirects, request-trusted authorization, shell
56
+ `-c` injection, ReDoS, zip slip, prototype pollution, GitHub Actions
57
+ injection and more.
58
+ - **It's fast when it matters.** A per-repository daemon keeps the TypeScript
59
+ checker, `dmypy` and build caches warm. A TypeScript check takes about 40 ms
60
+ warm instead of 500 ms.
61
+ - **It measures structural sloppiness too.** `loki.py slop` scores complexity,
62
+ duplication and import cycles in six languages and flags new hotspots as the
63
+ agent writes them.
64
+
65
+ ## How it compares
66
+
67
+ Head to head with [Interlinked CLI](https://github.com/QuentinCody/interlinked-cli)
68
+ (`82307ea`), both installed as Claude Code hooks. Each ran in a fresh sandboxed
69
+ repository per case and repetition. A defect counts only if the right diagnostic
70
+ appears on every repetition.
71
+
72
+ | Suite | Loki caught | Interlinked caught | Loki prevented before write | Loki false blocks | Interlinked false blocks |
73
+ | --- | ---: | ---: | ---: | ---: | ---: |
74
+ | **Blind holdout** (60 unseen cases, before any tuning) | **33** | 12 | **19** | 3 | 2 |
75
+ | Development corpus | **46/47** | 20/47 | **36** | **0** | 1 |
76
+ | Missing project-local JS tools | **17/17** | 13/17 | **17** | **0** | 1 |
77
+ | Holdout 1 (tuned) | **41/48** | 11/48 | **26** | **0** | 1 |
78
+ | Holdout 2 (tuned) | **42/60** | 12/60 | **29** | **0** | 2 |
79
+
80
+ Median hook latency is on par (402 ms vs 389 ms) with a much shorter tail
81
+ (p95 1.6 s vs 6.4 s). Without project-local JS tools, Loki takes 334 ms against
82
+ Interlinked's 10.8 s. Every holdout defect Interlinked catches, Loki catches too.
83
+
84
+ These are synthetic cases, not production sessions. The blind holdout was written
85
+ by an agent that could not see either product. Read the
86
+ [full results](https://github.com/alfredosdpiii/loki/blob/main/artifacts/comprehensive-summary-2026-09-26.md) and the
87
+ [benchmark protocol](https://github.com/alfredosdpiii/loki/blob/main/benchmarks/README.md).
88
+
89
+ ## Quick start
90
+
91
+ Loki needs Python 3.11 or newer and has no runtime dependencies. Install the
92
+ `loki` command, then point it at a Git repository:
93
+
94
+ ```bash
95
+ uv tool install loki-guardrails # or: pipx install loki-guardrails
96
+ loki init --dir /path/to/your/repo
97
+ ```
98
+
99
+ To try it without installing anything, run `uvx --from loki-guardrails loki init --dir .`.
100
+ You can also clone this repository and run `python3 loki.py init --dir /path/to/repo`.
101
+
102
+ That installs Loki into `.loki/`, registers hooks for every supported agent, and
103
+ adds starter configuration for Ruff, Oxlint, golangci-lint and a GitHub Actions
104
+ check. Existing configuration is merged or left alone.
105
+
106
+ Then, inside the repository:
107
+
108
+ ```bash
109
+ loki scan # check the working tree
110
+ loki scan --strict --base origin/main # CI: missing tools fail
111
+ loki slop # structural sloppiness report
112
+ ```
113
+
114
+ The installed hooks don't use the global `loki` command. `init` copies the
115
+ engine into `.loki/loki.py`, so every repository runs the exact version its
116
+ policy was reviewed against. CI runs the same file, and upgrades happen only
117
+ through `loki init --force` and a reviewed commit. `loki --version` and
118
+ `python3 .loki/loki.py --version` show which version each one is.
119
+
120
+ Codex needs a one-time `/hooks` approval and Pi a trust prompt. The
121
+ [install guide](https://github.com/alfredosdpiii/loki/blob/main/docs/reference.md#install) covers each host.
122
+
123
+ ## How it works
124
+
125
+ ```mermaid
126
+ flowchart LR
127
+ A[Agent proposes a write] --> B{Pre-write}
128
+ B -->|path, policy, secrets,<br/>security rules, Ruff,<br/>TypeScript types| C[Deny with the exact finding]
129
+ B -->|clean| D[File is written]
130
+ D --> E{Post-write}
131
+ E -->|mypy, golangci-lint, Clippy,<br/>Credo, Sobelow, Oxlint| F[Report net-new findings]
132
+ E --> G[Advisory: new hotspots,<br/>clones, import cycles]
133
+ H[(Warm daemon)] -. keeps analyzers warm .-> B
134
+ H -.-> E
135
+ ```
136
+
137
+ 1. **Before the write,** Loki reconstructs the file the agent is about to
138
+ produce. It checks the path against protected policy, runs the tool-free
139
+ content rules, and, where supported, runs Ruff and a TypeScript type check on
140
+ the proposed content. A denial carries the precise diagnostic.
141
+ 2. **After the write,** the language's analyzers run on what actually landed.
142
+ New findings go back to the agent, while existing debt stays quiet.
143
+ 3. **In CI,** `scan` repeats the checks against a base revision, including test
144
+ integrity, dependency policy and lockfile consistency.
145
+
146
+ Loki blocks its own policy files, hook registration and CI workflow from agent
147
+ edits, so an agent can't switch it off.
148
+
149
+ ## What it catches
150
+
151
+ | Language | On every write | Tool-free rules |
152
+ | --- | --- | --- |
153
+ | **Python** | Net-new Ruff and mypy (annotated code), import and AST checks | request-trusted authorization, SSRF, path traversal, shell `-c`, timing-unsafe compares, ReDoS, tautological tests |
154
+ | **TypeScript / JS** | Oxlint, project type check (pre-write), floating promises, numeric `.sort()` | XSS sinks, open redirects, `eval`, command and SQL injection, SSRF, prototype pollution, `postMessage` origin, timer leaks, ReDoS, disabled TLS |
155
+ | **Go** | gofmt, go vet, golangci-lint with gosec on changed lines | zip slip |
156
+ | **Rust** | rustfmt, net-new Clippy and compiler errors | placeholders, unchecked `get_unchecked`, unflushed `BufWriter`, shell `-c` |
157
+ | **Elixir / Phoenix** | mix format, net-new Credo and Sobelow | request-trusted authorization, SSRF, open redirects, structural `DateTime` sorts, unhandled `File` results, leaked handles |
158
+ | **Any file** | protected paths, root confinement | hardcoded credentials, merge-conflict markers, GitHub Actions script injection |
159
+
160
+ Missing analyzers are reported as `NOT CHECKED`, never as clean, and
161
+ `LOKI_STRICT=1` turns them into failures. The [reference](https://github.com/alfredosdpiii/loki/blob/main/docs/reference.md)
162
+ documents every rule, threshold and limitation.
163
+
164
+ ## Structural sloppiness
165
+
166
+ `loki.py slop` puts a single number on how hard code is to change. It combines
167
+ the erosion metric from
168
+ [Measuring code sloppiness](https://earendil.com/posts/measuring-code-sloppiness/)
169
+ with [trellis](https://github.com/jayminwest/trellis)'s scoring:
170
+
171
+ ```text
172
+ $ python3 .loki/loki.py slop
173
+ Sloppiness index: 26/100 (lower is better; trellis 0.2.0-provisional weights)
174
+ complexity-erosion 26.2 eroded share 0.94, 1 of 4 functions with CC > 10
175
+ duplication 0.0 density 0.00, 0 clone groups
176
+ import-cycle 0.0 density 0.00, 0 cycles
177
+ hotspot src/discounts.ts:1 discount CC 17, 15 SLOC, mass 65.8
178
+ ```
179
+
180
+ Complexity is exact for Python, TypeScript/JavaScript, Go and Elixir using each
181
+ language's own parser. It is approximated structurally for Rust, Java, Kotlin, C#,
182
+ C/C++, Swift, Scala, PHP and Ruby. Clones are detected on normalized tokens, so
183
+ renamed copies count. `--base` compares against a revision, and `max_index` or
184
+ `max_index_increase` can gate CI.
185
+
186
+ ## Configuration
187
+
188
+ Policy lives in `.loki/loki.json` and is always read from the committed base, so
189
+ an agent can't loosen it mid-session:
190
+
191
+ ```json
192
+ {
193
+ "rule_packs": ["core", "python", "typescript", "phoenix", "shell"],
194
+ "shell_commands": [
195
+ {"command": "npm test", "cwd": ".", "inputs": ["package.json", "package-lock.json"]}
196
+ ],
197
+ "slop": {"max_index": 40, "max_index_increase": 2, "block": false},
198
+ "elixir_security": {"sobelow": {"block": "high", "warn": "medium"}}
199
+ }
200
+ ```
201
+
202
+ | Setting | Default | Effect |
203
+ | --- | --- | --- |
204
+ | `typescript_check` | on when `tsconfig.json` exists | `false` disables it; `true` makes setup problems block |
205
+ | `slop.block` | `false` | Turns new hotspots, clones and cycles from advice into failures |
206
+ | `shell_commands` | none | Exact, reviewed shell commands the shell guard admits |
207
+ | `LOKI_STRICT=1` | off | Missing analyzers fail instead of reporting `NOT CHECKED` |
208
+ | `LOKI_DAEMON=0` | on | Runs every hook in-process |
209
+
210
+ ## Supported agents
211
+
212
+ | Agent | Integration |
213
+ | --- | --- |
214
+ | Claude Code | `.claude/settings.json` hooks |
215
+ | Codex | `.codex/hooks.json` hooks |
216
+ | Factory Droid | `.factory/hooks.json` hooks |
217
+ | Pi | `.pi/extensions/loki.ts` |
218
+ | OMP | `.omp/extensions/loki.ts` |
219
+
220
+ ## Documentation
221
+
222
+ - [Reference](https://github.com/alfredosdpiii/loki/blob/main/docs/reference.md): every check, rule, threshold and limitation
223
+ - [Benchmark protocol](https://github.com/alfredosdpiii/loki/blob/main/benchmarks/README.md) and the
224
+ [latest results](https://github.com/alfredosdpiii/loki/blob/main/artifacts/comprehensive-summary-2026-09-26.md)
225
+ - [Blind holdout cases](https://github.com/alfredosdpiii/loki/blob/main/benchmarks/holdout2_cases.json) and the
226
+ [runner](https://github.com/alfredosdpiii/loki/blob/main/benchmarks/holdout.py)
227
+
228
+ ## Development
229
+
230
+ ```bash
231
+ uv run --frozen pytest -q
232
+ bun test ./tests/loki-shim.test.ts ./tests/loki-omp.test.ts
233
+ ruff check --config templates/.ruff.toml loki.py tests benchmarks
234
+ ```
235
+
236
+ Regenerate the demo with `asciinema rec -c "bash docs/demo/demo.sh" docs/demo/demo.cast`
237
+ followed by `agg docs/demo/demo.cast docs/demo/demo.gif`.
@@ -0,0 +1,214 @@
1
+ <div align="center">
2
+
3
+ # Loki
4
+
5
+ **Deterministic guardrails for AI coding agents.**
6
+
7
+ Loki checks every edit your agent makes with real analyzers and precise rules. It
8
+ blocks the dangerous ones before the file changes, tells the agent exactly what
9
+ to fix, and does all of this without calling a model.
10
+
11
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776ab?logo=python&logoColor=white)
12
+ ![Runtime dependencies: none](https://img.shields.io/badge/runtime%20dependencies-none-2ea44f)
13
+ ![Agents](https://img.shields.io/badge/agents-Claude%20Code%20%C2%B7%20Codex%20%C2%B7%20Factory%20%C2%B7%20Pi%20%C2%B7%20OMP-8a63d2)
14
+ ![Languages](https://img.shields.io/badge/languages-Python%20%C2%B7%20TS%2FJS%20%C2%B7%20Go%20%C2%B7%20Rust%20%C2%B7%20Elixir-e36209)
15
+
16
+ <img src="https://raw.githubusercontent.com/alfredosdpiii/loki/main/docs/demo/demo.gif" alt="Loki blocking an XSS write, a leaked API key and a cross-file type error, then allowing the fix and flagging a complexity hotspot" width="820">
17
+
18
+ <sub>Real hook output from <a href="https://github.com/alfredosdpiii/loki/blob/main/docs/demo/demo.sh">docs/demo/demo.sh</a>. Only the typing is simulated.</sub>
19
+
20
+ </div>
21
+
22
+ ## Why Loki
23
+
24
+ - **It stops problems before they land.** Content rules, Ruff and a full
25
+ TypeScript type check run on the proposed file before the write. XSS, leaked
26
+ keys and broken callers never reach disk.
27
+ - **It only complains about what the edit changed.** Findings are compared
28
+ against the committed file by rule and source line. Existing debt and shifted
29
+ lines pass, so agents aren't blocked by code they didn't touch.
30
+ - **It uses real analyzers.** mypy, `tsc`, golangci-lint, Clippy, Credo and
31
+ Sobelow run on every write, all net-new. Twenty-six tool-free rules cover
32
+ what linters miss: SSRF, open redirects, request-trusted authorization, shell
33
+ `-c` injection, ReDoS, zip slip, prototype pollution, GitHub Actions
34
+ injection and more.
35
+ - **It's fast when it matters.** A per-repository daemon keeps the TypeScript
36
+ checker, `dmypy` and build caches warm. A TypeScript check takes about 40 ms
37
+ warm instead of 500 ms.
38
+ - **It measures structural sloppiness too.** `loki.py slop` scores complexity,
39
+ duplication and import cycles in six languages and flags new hotspots as the
40
+ agent writes them.
41
+
42
+ ## How it compares
43
+
44
+ Head to head with [Interlinked CLI](https://github.com/QuentinCody/interlinked-cli)
45
+ (`82307ea`), both installed as Claude Code hooks. Each ran in a fresh sandboxed
46
+ repository per case and repetition. A defect counts only if the right diagnostic
47
+ appears on every repetition.
48
+
49
+ | Suite | Loki caught | Interlinked caught | Loki prevented before write | Loki false blocks | Interlinked false blocks |
50
+ | --- | ---: | ---: | ---: | ---: | ---: |
51
+ | **Blind holdout** (60 unseen cases, before any tuning) | **33** | 12 | **19** | 3 | 2 |
52
+ | Development corpus | **46/47** | 20/47 | **36** | **0** | 1 |
53
+ | Missing project-local JS tools | **17/17** | 13/17 | **17** | **0** | 1 |
54
+ | Holdout 1 (tuned) | **41/48** | 11/48 | **26** | **0** | 1 |
55
+ | Holdout 2 (tuned) | **42/60** | 12/60 | **29** | **0** | 2 |
56
+
57
+ Median hook latency is on par (402 ms vs 389 ms) with a much shorter tail
58
+ (p95 1.6 s vs 6.4 s). Without project-local JS tools, Loki takes 334 ms against
59
+ Interlinked's 10.8 s. Every holdout defect Interlinked catches, Loki catches too.
60
+
61
+ These are synthetic cases, not production sessions. The blind holdout was written
62
+ by an agent that could not see either product. Read the
63
+ [full results](https://github.com/alfredosdpiii/loki/blob/main/artifacts/comprehensive-summary-2026-09-26.md) and the
64
+ [benchmark protocol](https://github.com/alfredosdpiii/loki/blob/main/benchmarks/README.md).
65
+
66
+ ## Quick start
67
+
68
+ Loki needs Python 3.11 or newer and has no runtime dependencies. Install the
69
+ `loki` command, then point it at a Git repository:
70
+
71
+ ```bash
72
+ uv tool install loki-guardrails # or: pipx install loki-guardrails
73
+ loki init --dir /path/to/your/repo
74
+ ```
75
+
76
+ To try it without installing anything, run `uvx --from loki-guardrails loki init --dir .`.
77
+ You can also clone this repository and run `python3 loki.py init --dir /path/to/repo`.
78
+
79
+ That installs Loki into `.loki/`, registers hooks for every supported agent, and
80
+ adds starter configuration for Ruff, Oxlint, golangci-lint and a GitHub Actions
81
+ check. Existing configuration is merged or left alone.
82
+
83
+ Then, inside the repository:
84
+
85
+ ```bash
86
+ loki scan # check the working tree
87
+ loki scan --strict --base origin/main # CI: missing tools fail
88
+ loki slop # structural sloppiness report
89
+ ```
90
+
91
+ The installed hooks don't use the global `loki` command. `init` copies the
92
+ engine into `.loki/loki.py`, so every repository runs the exact version its
93
+ policy was reviewed against. CI runs the same file, and upgrades happen only
94
+ through `loki init --force` and a reviewed commit. `loki --version` and
95
+ `python3 .loki/loki.py --version` show which version each one is.
96
+
97
+ Codex needs a one-time `/hooks` approval and Pi a trust prompt. The
98
+ [install guide](https://github.com/alfredosdpiii/loki/blob/main/docs/reference.md#install) covers each host.
99
+
100
+ ## How it works
101
+
102
+ ```mermaid
103
+ flowchart LR
104
+ A[Agent proposes a write] --> B{Pre-write}
105
+ B -->|path, policy, secrets,<br/>security rules, Ruff,<br/>TypeScript types| C[Deny with the exact finding]
106
+ B -->|clean| D[File is written]
107
+ D --> E{Post-write}
108
+ E -->|mypy, golangci-lint, Clippy,<br/>Credo, Sobelow, Oxlint| F[Report net-new findings]
109
+ E --> G[Advisory: new hotspots,<br/>clones, import cycles]
110
+ H[(Warm daemon)] -. keeps analyzers warm .-> B
111
+ H -.-> E
112
+ ```
113
+
114
+ 1. **Before the write,** Loki reconstructs the file the agent is about to
115
+ produce. It checks the path against protected policy, runs the tool-free
116
+ content rules, and, where supported, runs Ruff and a TypeScript type check on
117
+ the proposed content. A denial carries the precise diagnostic.
118
+ 2. **After the write,** the language's analyzers run on what actually landed.
119
+ New findings go back to the agent, while existing debt stays quiet.
120
+ 3. **In CI,** `scan` repeats the checks against a base revision, including test
121
+ integrity, dependency policy and lockfile consistency.
122
+
123
+ Loki blocks its own policy files, hook registration and CI workflow from agent
124
+ edits, so an agent can't switch it off.
125
+
126
+ ## What it catches
127
+
128
+ | Language | On every write | Tool-free rules |
129
+ | --- | --- | --- |
130
+ | **Python** | Net-new Ruff and mypy (annotated code), import and AST checks | request-trusted authorization, SSRF, path traversal, shell `-c`, timing-unsafe compares, ReDoS, tautological tests |
131
+ | **TypeScript / JS** | Oxlint, project type check (pre-write), floating promises, numeric `.sort()` | XSS sinks, open redirects, `eval`, command and SQL injection, SSRF, prototype pollution, `postMessage` origin, timer leaks, ReDoS, disabled TLS |
132
+ | **Go** | gofmt, go vet, golangci-lint with gosec on changed lines | zip slip |
133
+ | **Rust** | rustfmt, net-new Clippy and compiler errors | placeholders, unchecked `get_unchecked`, unflushed `BufWriter`, shell `-c` |
134
+ | **Elixir / Phoenix** | mix format, net-new Credo and Sobelow | request-trusted authorization, SSRF, open redirects, structural `DateTime` sorts, unhandled `File` results, leaked handles |
135
+ | **Any file** | protected paths, root confinement | hardcoded credentials, merge-conflict markers, GitHub Actions script injection |
136
+
137
+ Missing analyzers are reported as `NOT CHECKED`, never as clean, and
138
+ `LOKI_STRICT=1` turns them into failures. The [reference](https://github.com/alfredosdpiii/loki/blob/main/docs/reference.md)
139
+ documents every rule, threshold and limitation.
140
+
141
+ ## Structural sloppiness
142
+
143
+ `loki.py slop` puts a single number on how hard code is to change. It combines
144
+ the erosion metric from
145
+ [Measuring code sloppiness](https://earendil.com/posts/measuring-code-sloppiness/)
146
+ with [trellis](https://github.com/jayminwest/trellis)'s scoring:
147
+
148
+ ```text
149
+ $ python3 .loki/loki.py slop
150
+ Sloppiness index: 26/100 (lower is better; trellis 0.2.0-provisional weights)
151
+ complexity-erosion 26.2 eroded share 0.94, 1 of 4 functions with CC > 10
152
+ duplication 0.0 density 0.00, 0 clone groups
153
+ import-cycle 0.0 density 0.00, 0 cycles
154
+ hotspot src/discounts.ts:1 discount CC 17, 15 SLOC, mass 65.8
155
+ ```
156
+
157
+ Complexity is exact for Python, TypeScript/JavaScript, Go and Elixir using each
158
+ language's own parser. It is approximated structurally for Rust, Java, Kotlin, C#,
159
+ C/C++, Swift, Scala, PHP and Ruby. Clones are detected on normalized tokens, so
160
+ renamed copies count. `--base` compares against a revision, and `max_index` or
161
+ `max_index_increase` can gate CI.
162
+
163
+ ## Configuration
164
+
165
+ Policy lives in `.loki/loki.json` and is always read from the committed base, so
166
+ an agent can't loosen it mid-session:
167
+
168
+ ```json
169
+ {
170
+ "rule_packs": ["core", "python", "typescript", "phoenix", "shell"],
171
+ "shell_commands": [
172
+ {"command": "npm test", "cwd": ".", "inputs": ["package.json", "package-lock.json"]}
173
+ ],
174
+ "slop": {"max_index": 40, "max_index_increase": 2, "block": false},
175
+ "elixir_security": {"sobelow": {"block": "high", "warn": "medium"}}
176
+ }
177
+ ```
178
+
179
+ | Setting | Default | Effect |
180
+ | --- | --- | --- |
181
+ | `typescript_check` | on when `tsconfig.json` exists | `false` disables it; `true` makes setup problems block |
182
+ | `slop.block` | `false` | Turns new hotspots, clones and cycles from advice into failures |
183
+ | `shell_commands` | none | Exact, reviewed shell commands the shell guard admits |
184
+ | `LOKI_STRICT=1` | off | Missing analyzers fail instead of reporting `NOT CHECKED` |
185
+ | `LOKI_DAEMON=0` | on | Runs every hook in-process |
186
+
187
+ ## Supported agents
188
+
189
+ | Agent | Integration |
190
+ | --- | --- |
191
+ | Claude Code | `.claude/settings.json` hooks |
192
+ | Codex | `.codex/hooks.json` hooks |
193
+ | Factory Droid | `.factory/hooks.json` hooks |
194
+ | Pi | `.pi/extensions/loki.ts` |
195
+ | OMP | `.omp/extensions/loki.ts` |
196
+
197
+ ## Documentation
198
+
199
+ - [Reference](https://github.com/alfredosdpiii/loki/blob/main/docs/reference.md): every check, rule, threshold and limitation
200
+ - [Benchmark protocol](https://github.com/alfredosdpiii/loki/blob/main/benchmarks/README.md) and the
201
+ [latest results](https://github.com/alfredosdpiii/loki/blob/main/artifacts/comprehensive-summary-2026-09-26.md)
202
+ - [Blind holdout cases](https://github.com/alfredosdpiii/loki/blob/main/benchmarks/holdout2_cases.json) and the
203
+ [runner](https://github.com/alfredosdpiii/loki/blob/main/benchmarks/holdout.py)
204
+
205
+ ## Development
206
+
207
+ ```bash
208
+ uv run --frozen pytest -q
209
+ bun test ./tests/loki-shim.test.ts ./tests/loki-omp.test.ts
210
+ ruff check --config templates/.ruff.toml loki.py tests benchmarks
211
+ ```
212
+
213
+ Regenerate the demo with `asciinema rec -c "bash docs/demo/demo.sh" docs/demo/demo.cast`
214
+ followed by `agg docs/demo/demo.cast docs/demo/demo.gif`.
@@ -0,0 +1,161 @@
1
+ # Benchmark protocol
2
+
3
+ The earlier pre-write comparison does not establish an overall winner. It leaves
4
+ out checks that run after edits, whole-project analysis, and ordinary repair work.
5
+
6
+ `comprehensive.py` invokes the installed Claude command hooks for both products.
7
+ It writes an admitted source edit into a disposable repository, then invokes the
8
+ installed post-write handlers. It never executes proposed shell commands or
9
+ application code. Explicit audit commands may compile the controlled fixtures.
10
+
11
+ ## Cases and scoring
12
+
13
+ `corpus.py` contains paired defects and controls, exact replacements, cross-file
14
+ contract changes, existing-debt shifts, break/repair sequences, protected-path
15
+ attempts, and 10/1,000/10,000-file workloads. Languages are Python, TypeScript,
16
+ JavaScript, Go, Rust, and Elixir/Phoenix.
17
+
18
+ The corpus deliberately includes semantic security cases that neither product may
19
+ recognize. SSRF and request-controlled authorization stay in the denominator.
20
+ Cases reflect the author's knowledge of both products. They are synthetic, not an
21
+ independent holdout or a representative sample of production changes.
22
+
23
+ Before running, the runner saves the complete case list, its hash, the randomized
24
+ schedule, tool versions, command line, and source hashes. Each result uses an
25
+ exclusive file creation. Existing evidence cannot be replaced by another run.
26
+ Source identity is checked again when the run finishes.
27
+
28
+ The scorecard distinguishes:
29
+
30
+ - **Prevention:** a pre-hook denies the operation and emits a matching diagnostic.
31
+ - **Post-write detection:** a matching diagnostic reaches the host after source
32
+ bytes land. This is not prevention, rollback, or an OS security boundary.
33
+ - **Unrelated blocking:** denial without evidence of the intended defect.
34
+ - **False blocking:** a control is blocked in any repetition, before or after writing.
35
+ - **Unavailable checks:** reported separately, even if the overall hook admits the edit.
36
+ - **Infrastructure failure:** remains in the denominator and earns no detection credit.
37
+
38
+ An accuracy result needs the same successful detection on every repetition.
39
+ Three repetitions are three timing/stability samples, not three independent cases.
40
+ Exit-zero native JSON denials count as denials. Exit-zero stderr alone does not
41
+ count as feedback delivered to Claude. Audit scoring examines actual diagnostic
42
+ entries, not the names of checks that reported zero findings.
43
+
44
+ The scorecard also decodes JSON feedback before matching individual diagnostic
45
+ lines. Otherwise a regex can span escaped `\n` sequences and mistake unrelated
46
+ advice for a compiler finding. Node module-format notices do not count as syntax
47
+ detection. Raw records retain provisional outcomes; the scorecard lists every
48
+ correction and hashes the final scorer.
49
+
50
+ Control feedback includes missing-tool notices and advisory messages. It is not
51
+ automatically a false positive. The report preserves the raw output for review.
52
+ Cold and startup-warmed timings are separate. Each repetition gets a fresh
53
+ repository, home, and daemon, avoiding leaked reservations and diagnostic caches.
54
+ The first trial has no warmup. Later trials send two git-status pre-hook envelopes
55
+ before the case, without executing the commands. That warms startup, not compiler
56
+ result caches. "Cold" does not mean flushed OS caches. Latency includes process
57
+ and sandbox startup.
58
+
59
+ ## Isolation and tool parity
60
+
61
+ Each product/case/profile gets a new repository and home. Bubblewrap denies
62
+ network access, mounts the host filesystem read-only, and allows writes only to
63
+ the fixture tree and private temporary storage. There is no unsandboxed fallback.
64
+ The runner starts only known local binaries and uses an environment allowlist.
65
+ It uses the user's existing Git identity for fixture commits.
66
+
67
+ Repositories live under the ignored `artifacts/tmp/b/` directory by default,
68
+ outside OS temporary roots. Interlinked applies a distinct scratchpad policy to
69
+ paths under `/tmp`; measuring that policy would not represent ordinary repository
70
+ edits. `--work-dir` overrides the fixture parent. Keep its path short enough for
71
+ Unix domain sockets.
72
+
73
+ Both products receive the same analyzer paths. The shared profile supplies
74
+ TypeScript, Oxlint, Biome with its recommended lint config, Ruff, mypy,
75
+ golangci-lint, Go, Rust, and prepared Phoenix dependencies. This does not mean
76
+ both products invoke every available analyzer.
77
+
78
+ Loki's shell guard and a committed exact `npm test` permission are enabled.
79
+ Interlinked uses its balanced project installation. Other Loki settings remain
80
+ at their shipped values, including its disabled-by-default project type checker.
81
+ `configured.py` repeats the TypeScript cases with `"typescript_check": true`.
82
+ Keep that result separate from the ordinary installation.
83
+
84
+ The `missing-js` profile omits project-local JS analyzer links. It does not remove
85
+ system-wide tools or the competitor's own packaged dependencies. It measures that
86
+ specific degradation, not a completely tool-free machine.
87
+
88
+ Go/Rust audit results use `loki scan --base HEAD` and `interlinked verify --json`.
89
+ These have different product-defined scopes. Their findings are never added to
90
+ the ordinary-hook detection total.
91
+
92
+ The shared-scaffolding matrix places a JS package manifest in every fixture to
93
+ support its shell-grant controls. That can affect project discovery in non-JS
94
+ repositories. `native_projects.py` is a separate sensitivity run: Python gets
95
+ `pyproject.toml` with Ruff and mypy sections, and Go/Rust/Phoenix retain only their
96
+ native manifests. It omits the irrelevant npm grant for those fixtures. Do not
97
+ pool its observations with the shared-scaffolding results.
98
+
99
+ ## Reproduce
100
+
101
+ Prepare the toolchains first, outside the network-isolated run. The competitor
102
+ checkout must be clean at `207330d8131c5203ecc74e9fd4c24ba416463718`, with its
103
+ `dist/` build present. The Phoenix project must use
104
+ `fixtures/phoenix/mix.exs` and `mix.lock`, with dependencies compiled under
105
+ Elixir 1.17.3 and a compatible OTP 27 installation. Provide a compatible Hex
106
+ archive as well. No global toolchain setting needs to change.
107
+
108
+ Run from the Loki checkout, substituting absolute paths:
109
+
110
+ ```sh
111
+ ERL_FLAGS='+S 4:4' /usr/bin/python3 benchmarks/comprehensive.py \
112
+ --interlinked /path/to/interlinked \
113
+ --node /path/to/node-23/bin/node \
114
+ --oxlint /path/to/oxlint-tools/node_modules/.bin/oxlint \
115
+ --typescript /path/to/typescript-tools/node_modules/typescript \
116
+ --biome /path/to/biome-tools/node_modules/.bin/biome \
117
+ --python-tools /path/to/mypy-venv/bin \
118
+ --golangci /path/to/golangci-lint \
119
+ --phoenix /path/to/prepared-phoenix \
120
+ --hex-archive /path/to/hex-archive \
121
+ --erlang-bin /path/to/otp-27/bin \
122
+ --runs 3 --seed 20260921 --profiles shared missing-js --scan \
123
+ --output /path/to/new-evidence-directory
124
+
125
+ python3 -m benchmarks.report_comprehensive /path/to/new-evidence-directory
126
+ ```
127
+
128
+ Use an actual Python executable, rather than a toolchain shim that might rewrite
129
+ `PATH` during startup. The explicit `--erlang-bin` also prevents that environment
130
+ problem inside fixtures.
131
+
132
+ For the type-check-enabled run, replace `comprehensive.py` with `configured.py`,
133
+ use `--profiles shared --case typescript`, and choose a new output directory.
134
+ That entry point changes only accepted fixture policy, not either product's code.
135
+ Its own hash is included in the evidence identity.
136
+
137
+ For native project discovery, use `native_projects.py --profiles shared` and
138
+ `--case '(^python/|^go/|^rust/|^elixir/|/python/)'`, with the same tool arguments
139
+ and a new output directory. This tests configuration sensitivity without changing
140
+ either product or the defect/control source.
141
+
142
+ ## Separate integration and regression suites
143
+
144
+ - `host_context.py`: real installed hosts with fresh homes and a loopback model
145
+ stub, with hooks enabled and disabled. This proves startup discovery/context
146
+ delivery, not live-model tool enforcement.
147
+ - `phoenix.py`: installed ordinary hooks plus project-tier compilation and HEEx
148
+ checks under multiple Sobelow confidence thresholds.
149
+ - `language_rules.py`: targeted analyzer defect/repair regressions. These were
150
+ developed with Loki's policies and are not independent comparison cases.
151
+ - `tests/loki-shim.test.ts` and `tests/loki-omp.test.ts`: actual checker subprocesses
152
+ through adapter callbacks, not running host inference sessions.
153
+
154
+ The smoke-run directories preserve setup failures. They are not merged into the
155
+ final comparison. Do not silently discard failures from a completed final run or
156
+ change cases and then reuse its manifest.
157
+
158
+ This protocol still does not measure long-running autonomous sessions, production
159
+ incident prevention, online dependency checks, all supported host write formats,
160
+ or unknown attacks. Those require separate experiments and explicit authorization
161
+ before using private repositories or paid model endpoints.