Report privately through GitHub's Report a vulnerability form, or by email to kmike1337@gmail.com. Please do not open a public issue.
Expect an acknowledgement within a week. metron is maintained by one person as a side project, so please size your expectations accordingly — but a real vulnerability will get a real fix.
Only the latest release. metron is pre-1.0 and there are no maintenance branches.
Read this before deciding how much to trust it. metron is a program that executes changes proposed by a language model against your working tree. That is the whole point of it, and it means the threat model is unusual.
By design:
apply_patchwrites to files in your working directory viagit apply.- Every patch is shown to you as a diff and requires an explicit
ybefore it is applied. End-of-input answers no — an operator who has walked away has not consented. auto_approve_patches: truein config, and the--yesflag, disable that prompt entirely. Both exist for scripted use. Do not set them on a repository whose contents you would mind losing, and prefer running against a clean git tree so that anything unwanted is onegit checkoutaway.-p/--promptwithout--yesfails closed: nobody is at the keyboard to approve, so patches are refused rather than applied unattended.- A project's config file is untrusted.
<repo>/.metron.jsonis data the repository ships, so it may tune budgets and the model but may not enableauto_approve_patchesor grantallowed_commands. A project file that asks for either is refused with a warning on stderr at startup and in--doctor, and the operator can permit a project'sallowed_commandsfor a run by settingMETRON_ALLOW_PROJECT_COMMANDSto1(also accepted:true,yes). The operator's own config --~/.metron/config.jsonor the fileMETRON_CONFIGnames -- is trusted and may set either, as may theOLLAMA_HOSTandOLLAMA_MODELenvironment overrides. run_commandexecutes commands in your project. It is off by default: with noallowed_commandsset, the tool is not offered to the model at all and its schema is not even sent. Turning it on is a deliberate act.
Confinement. Every path a tool touches is resolved against the project root
-- the enclosing git work tree, or the working directory if there is none -- and
refused if it lands outside. Symlinks are followed before the check, so a
symlinked directory cannot be used to step out of the tree, and that holds for
files being created as well as read, since the check resolves the ancestors of
a path that does not exist yet. view_slice will not read ~/.ssh/id_rsa, and
apply_patch will not write to ../...
For patches this is belt and braces rather than the only guard: git apply
already rejects paths containing .., refuses to follow a symlinked directory
out of the tree, and treats a leading / as relative to the tree rather than to
the filesystem. metron checks anyway, so the boundary is stated in metron's own
terms and survives a future flag or backend that loosens git's.
Running commands. run_command is the only tool that can cause an effect
metron cannot describe in advance, so it is bounded four ways:
- There is no shell. The command is split on whitespace and executed
directly.
;,&&,|, redirection and globs are never interpreted -- they arrive as literal arguments and the program rejects them. This is the security property the design rests on: not a blocklist of dangerous characters, but never handing the string to anything that would interpret them. - An allowlist decides what may run at all, matched on whole argv tokens.
"go test"permitsgo test ./...and refusesgo tool,gotcha test,go --work testandenv go test. Matching per element rather than over the joined string is what makes it hard to talk around. - You are asked before it runs, with the same prompt apply_patch uses, and the same fail-closed behaviour on end-of-input.
- It is bounded in time and output. The command runs in its own process
group and the whole group is killed at
command_timeout_seconds, so ago testthat spawns a test binary does not outlive its deadline. Output is clipped tomax_command_output_bytes.
Choose allowlist entries with the same care you would give a sudoers file. A
broad entry is a broad grant: "go" permits go run ./anything and, through
go generate, code the repository itself supplies; "make" runs whatever the
project's Makefile says. An allowlisted command is code the repository chose,
so choose entries with the care you would give a sudoers file and prefer the
narrowest prefix that does the job.
Remaining limitations -- these are real:
- Anything the model reads can reach the model's operator. If you point
metron at a repository containing secrets, and the model reads them, they are
in the conversation. With a local Ollama server that conversation does not
leave your machine; that property is a consequence of your configuration, not
something metron enforces. metron does guard the path: the endpoint is checked
to be an http(s) URL with a host before any request is built (so a config
pointing at
file://,ftp://, or a host-less endpoint cannot be turned into an exfiltration channel), and metron warns on stderr whenever the configured endpoint is not reached over loopback -- since that is the moment the conversation starts leaving the machine, over a transport an attacker may be able to read. Every redirect an endpoint issues is revalidated the same way: it must retain the original host and scheme, so a different host, an HTTPS-to-HTTP downgrade, or any non-http(s) scheme is refused and a misconfigured or hostile server cannot send the request somewhere else. - Confinement is the project directory, not a sandbox. Everything inside the project is fair game, including files you would rather the model not read. There is no per-file policy and no allowlist.
- An allowed command is not confined.
run_commandsets the working directory to the project, but the command itself runs with your full user privileges and can reach anything you can. Path confinement bounds metron's own tools; it cannot bound a program you have permitted to run. - An allowed command runs with your full environment.
run_commandinherits your process environment as-is, so credentials you export -- a$GITHUB_TOKEN,AWS_*variables, a login shell'sPATH-- are visible to anything the model is permitted to run. Path confinement bounds metron's own tools; it does not scrub the environment of a program you allowed. list_fileshides gitignored paths, butview_slicewill still read them.list_fileswalks withrg --filesand so skips what your.gitignoreexcludes;view_slicereads a named path directly and is not subject to.gitignore, so a path a project ignores -- a local.env, a scratch file -- is readable by name.- Path confinement is not atomic. metron resolves a path and checks that it is
inside the project, then hands it to
git apply, and those are two separate steps. A symlink could in principle be swapped between the check and the apply -- the classic time-of-check-to-time-of-use gap. metron's own check andgit apply --checktogether make this hard to exploit, but metron does not claim to close it completely. - Prompt injection is not mitigated. Content in the files metron reads is data, but a sufficiently persuasive comment in a source file may influence what the model proposes. The approval prompt is the mitigation. Read the diffs.
Not in scope: the security of the model you point metron at, the security of your Ollama server, or model output quality. metron does not sandbox model-proposed changes beyond the approval prompt, and does not claim to.
- No telemetry, no analytics, no crash reporting, no network calls other than to the endpoint you configure.
- The endpoint a config names is checked to be an http(s) URL with a host before
any request is built; a
file://,ftp://or host-less endpoint cannot be turned into a network call, a non-loopback target is announced on stderr so a config that ships the conversation away is not a silent one, and any redirect the endpoint issues must retain the original host and scheme. - No credentials of any kind are read or stored. There is no login and no account.
- No shell. External binaries (
rg,ctags,git) are invoked with an explicit argument vector, never throughsh -c, so a model-supplied string cannot become a shell command. Model-supplied values are additionally kept out of the flag namespace -- passed after--or as--flag=value-- so a pattern beginning with a dash is data rather than an option.