Please report security vulnerabilities privately rather than opening a public issue. Use GitHub's "Report a vulnerability" (Security Advisories) on the repository, or email the security contact, technologistvenkat@gmail.com. We aim to acknowledge reports within a few days.
kritya is beta software. The properties below are implemented and covered by
automated checks (npm audit, Dependabot), but have not had a third-party
security audit or penetration test — treat them as defense-in-depth, not a
certified guarantee.
CI runs npm audit through a reviewed gate
(scripts/check-audit.mjs) rather than a bare --audit-level=high, and the
package is also monitored on Socket
under an org-level Team plan, with dependency reachability analysis enabled —
Socket auto-determines whether a vulnerable transitive dependency path is
actually reachable from kritya's code, rather than requiring a hand-written
reachability argument for every new finding. Findings from both fall into two
buckets:
- High-severity CVEs with no available fix are recorded in
scripts/audit-allowlist.jsonwith a reachability argument — why the vulnerable code path isn't reachable from kritya's own code — and an expiry date that forces a re-check. The list is currently empty. It previously carried two advisories inimage-size's ICNS/JXL/HEIF parsers (GHSA-w3rx-r6r6-pgpr, GHSA-5p2g-fcmc-qvqq), pulled in transitively viapptxgenjs: those parsers are only reached throughpptxgenjs'saddImage(), which kritya never calls. That case was resolved rather than tolerated — no 1.ximage-sizeafter 1.2.1 exists andpptxgenjsstill pins^1.2.1on its latest release, so anoverridesentry forcesimage-size ^2.0.4. The override is safe precisely because the dependency is never loaded:image-sizeis declared bypptxgenjsbut absent from all three of its shipped bundles, and drivingaddImage()under a module-load hook never requests it. A third entry coveredhttp-cache-semanticsin the build-time-onlyelectron-builderchain and was cleared by a4.3.0bump once that release shipped. - Socket's supply-chain heuristics ("obfuscated code," "deprecated,"
"AI-detected anomaly," etc.) are reviewed rather than acted on
automatically, since they flag patterns, not confirmed exploits:
- "Obfuscated code" on
exceljs,pdf-lib,underscore,electron-winstaller,tiny-async-pool, andyargsis a false positive on normal packaging, not real obfuscation — checked directly: none of the flagged files show minification or bundling signatures (longest lines run 81-1540 characters of readable code, consistent with TypeScript-compiled output or a bundleddistshipped for browser use, not mangled/packed code).exceljs,pdf-lib, andunderscoreare already pinned to their latest published version, and higher-profile alternatives in this space (e.g. SheetJS/xlsxin place ofexceljs) carry a worse CVE history while triggering the same flag.electron-winstaller,tiny-async-pool, andyargs(the latter also viac8) are transitiveelectron-builder/dev-tooling dependencies that never ship in the published package (files: ["dist"]inpackage.jsonexcludesdist/testanddist/electron). - "Deprecated" on
booleanandrimraf@2.6.3traces toelectron-builder's own dependency tree (via@electron/get→global-agent, andelectron-builder-squirrel-windows→electron-winstaller), 4-5 levels deep.electron-builderis already on its latest release; this is a maintainer-status flag on transitive deps kritya doesn't control, not a vulnerability. @xmldom/xmldom@0.8.15carries a maintainer deprecation notice ("this version has critical issues, please update to the latest version"). The latest release,0.9.12, was tried via anoverridespin and reverted:0.9.xrequires an explicitmimeTypeargument toDOMParser.parseFromString, whichmammoth@1.12.2(itself the latest release, and the package that pullsxmldomin for.docxparsing) calls without one, breaking.docxreading (3 test failures indocument.test.js).mammothstill declares^0.8.6forxmldom, so there is currently no combination that gets the deprecation fix without breaking.docxsupport. Revisit oncemammothadapts to the newxmldomAPI.
- "Obfuscated code" on
engines.node is >=22.23.4, and that is a security floor rather than a
language-feature one. Node's bundled Brotli decoder carried an out-of-bounds
write that malformed compressed input could turn into remote code execution
(CVE-2026-111497), and kritya reaches that decoder on a path it cannot
avoid: fetch_url pulls arbitrary public URLs and the provider client streams
responses, both through undici, whose fetch advertises br and decompresses
it transparently via node:zlib. A hostile page the agent visits is therefore
untrusted Brotli data arriving inside kritya's own process — the one place the
SSRF guard above does not help, because it constrains where kritya fetches,
not what comes back.
The fix is upstream, in the Node release: 22.23.4, 24.22.1, and 26.12.1 (all released 2026-10-21). Node ships no in-runtime workaround, so there is nothing kritya can do in its own code beyond requiring a patched runtime.
Two limits worth stating plainly:
- A single
>=floor cannot express the per-line requirement. 24.0.0 through 24.22.0 and 26.0.0 through 26.12.0 are affected too, but they compare greater than 22.23.4 and so satisfyengines.node. That is why the gate is notenginesitself:npm run check:node(CI and before publish) andkritya doctorboth enforce the patched minimum per line — 22.23.4, 24.22.1 and 26.12.1 respectively — and reject the end-of-life lines in between (23, 25), which never received a fix.enginesstays as the coarse signal npm itself shows. enginesis a warning, not a gate. npm reports EBADENGINE and installs anyway, and a container image or system package manager can pin a Node that kritya never inspects. A package inventory ofnode_moduleswill not reveal it either, because the vulnerable code is in the interpreter, not in a dependency.
The Brotli request-body path — the other way into that decoder — does not apply: kritya runs no Brotli body-parsing middleware, and its only HTTP servers are the localhost MCP OAuth callback and test fixtures.
In October 2026, eight eMudhra intermediate CA private keys were recovered
from public certificate data — the keys were small enough to enumerate, so
anyone could have issued certificates those intermediates would vouch for
(CVE-2026-111943). Browsers blocked the intermediates through their own
revocation channels, but that does not cover an application that carries its
own TLS stack — and the Electron desktop build is exactly that: it bundles
its own Chromium networking and its own Node.js tls/https/fetch, none
of which follow the OS browser's block list.
Electron shipped the block for the compromised intermediates in 44.9.1 (CVE-2026-115570), covering both the Chromium and the Node.js paths. kritya's desktop build is on Electron 44.9.3, so current builds already reject those certificates on both paths. Two consequences worth stating plainly:
- A desktop binary built with Electron older than 44.9.1 stays affected until it is rebuilt and redistributed. Updating Chrome, the OS, or the npm-installed CLI on the same machine does not change the copy inside an already-built desktop app.
- The CLI install carries no such copy. The npm package bundles no Electron and no certificate store; its TLS trust comes from the running Node.js, which is the runtime section above.
kritya is enrolled in Anthropic's OSS Scanner
(enrolment PR,
projects/kritya).
The scanner builds kritya in an isolated VM and analyses it with no network
access, emailing findings with reproducers and proposed patches to the security
contact above. The build image and threat model it uses live in this repository
at .oss-scanner/: Dockerfile and threat_model.md.
Reports from that service are model-generated and not human-reviewed, so they
are triaged the same way as any other finding — see "Reporting a vulnerability"
above. Findings that overlap the dependency allowlist in
scripts/audit-allowlist.json are documented there.
kritya runs an autonomous agent that can read and modify files and run shell commands in the workspace you launch it in. Some safety properties to be aware of:
- Permission prompts gate every mutating tool (
write_file,edit_file,shell) unless you allowlist them insettings.json. - Deny rules (
denyinsettings.json) block matching tool calls outright and cannot be overridden by an allow rule or an "always allow" choice. - Destructive-command detection forces a warning prompt for commands like
rm -rf,git push --force, andcurl | sh, even when allowlisted. This is a regex-based backstop, not a guarantee — it can be evaded (e.g. long-form flags like--recursive --force, or obfuscation via$(...),eval, or a base64-encoded payload). Don't rely on it as the sole safeguard for a blanketshell(*)allow rule. - Writes that would run code later force a prompt of their own: git hooks,
.husky/*,.git/config,.envrc, VS Code tasks,.pre-commit-config.yaml, and apackage.jsonwrite that adds a lifecycle script — plus shell redirects into any of them. This covers the one gap the sandbox cannot: the workspace is writable by design, so a hook planted now executes on your nextgit commit, outside the sandbox. A session also ends with a list of every such file it touched, which catches what the prompt missed. See Files that run code later. - Sandboxed execution (
sandboxExecin config — see Sandboxed execution) adds an OS-enforced backstop for the case above: shell commands run underbwrap(Linux),sandbox-exec(macOS), or MXC (Microsoft Execution Containers, Windows) with writes confined to the workspace, so evading the regex no longer means unrestricted write access to the rest of the machine. On Windows the backend is an optional install — a ProcessContainer (AppContainer + DACLs) enforced by the kernel, located viaMXC_BIN_DIRorKRITYA_MXC_EXEC. Default is"auto", which sandboxes every command on Linux/macOS when the required binary is present, and on Windows sandboxes every command once MXC is present (falling back to only commands flagged as dangerous when it isn't);"always"and"strict"sandbox every command on every platform. On Windows the default is"strict", so a host without MXC refuses commands rather than running them unconfined. Reads are open apart from a short credential deny list — SSH private keys,~/.aws,~/.config/gcloud,~/.kube,~/.docker/config.json,~/.netrc— which is enforced on Linux and macOS and not on Windows, where MXC's grant-based policy has no way to deny a path inside the system drive it must grant for the container to start. See Credential reads. Three credential files stay readable on purpose because a first-class workflow reads each one (~/.npmrc,~/.config/gh/hosts.yml,~/.git-credentials); they are live tokens in plain files, sosandboxReadDenyStrictmasks them as well. Network access is closed by default and blocks egress on all three backends;/network onopens it for the rest of a session and"sandboxNetwork": "open"makes that permanent. The grant is session-wide, not per-host — see Network. Headless runs take both postures unconditionally, since there is nobody to make that call mid-run — see Headless and CI runs. That setting covers the agent's own shell commands only: commands you run in your own terminal are never sandboxed, and kritya's web search and fetch run outside it too. Treat all of it as raising the cost of an evasion, not eliminating one."auto"and"always"fall back to an unsandboxed run (with a warning) if the sandbox binary isn't available;"strict"refuses to run the command at all in that case instead. - File access is confined to the workspace root — including via a symlink
inside the workspace that points outside it — and paths that look like
secrets (
.env*,.git/config,*credentials*,*secret*, private keys) are blocked from being read or written by tools, regardless of allowlist rules. Theshelltool gets the same filename-based check: a command whose text references a sensitive path (cat .env,grep foo .env) is refused rather than relying on output redaction alone. That check can only see the literal filename in the command text, so shell expansion or unusual quoting can slip past it. - Secret scanning on write. Filename checks only cover secrets kept in
files that look sensitive; they do nothing to stop the model writing a
real key it saw in some tool output into an ordinary file like
README.md. Sowrite_fileandedit_filealso scan the content being written for known key formats (AWS, GitHub, GitLab, Slack, Stripe, Google, OpenAI, Anthropic, npm, PyPI, Azure, GCP, private key blocks, …) and high-entropy secret-shaped assignments, and block the write. Shell and background-process output is redacted with the same patterns before it reaches the transcript. Both are heuristics — they can miss a novel key format and can false-positive on random-looking fixtures. - SSRF guard.
fetch_urland the MCP HTTP transport share one host check that refuses private, loopback, link-local, and carrier-grade-NAT ranges, so neither can be steered into your internal network or a cloud metadata endpoint. - Workspace trust. A repository's own
.kritya/settings.jsonallow rules, hooks,.env, custom commands,.mcp.json, and.kritya/plugins/are all inert until you trust that workspace, so cloning a hostile repo can't self-grant permissions or run code just by being opened. Trust is keyed on a hash of the gated content, so changing it re-prompts. In headless/CI mode this is off unless--trustis passed, since CI often checks out untrusted branches and PRs. - Per-server MCP trust. Every MCP server is arbitrary code (stdio) or a
remote endpoint (HTTP) running with your credentials, so each one also
gets its own first-use confirmation on top of workspace trust — approving a
repo's
.mcp.jsononce does not blanket-approve servers a later commit adds. The fingerprint covers the declared, pre-expansion config (never expanded env/header values, which may hold live secrets); if a server's command, args, cwd, url, tool filter, or env/header key names change, it counts as new and re-prompts./mcp trustlists approvals and/mcp trust revoke <name>withdraws them. - Agent Plugins are covered by both gates: a workspace plugin is only discovered once the workspace is trusted, and any MCP server it declares still needs its own per-server approval. See Agent Plugins.
- Untrusted content from web search and MCP tools is wrapped in explicit
markers, and the system prompt instructs the model to treat all tool output
as data, never as instructions. Prompt injection via file/command/web content
is nonetheless a real risk with any LLM agent — review changes before trusting
them, and use plan mode (
/plan) for unfamiliar repositories. - Subagents inherit these limits rather than escaping them. Read-only subagents get no write, edit, or shell tool at all; write-capable subagents are isolated on their own git worktree/branch and have destructive commands blocked outright, since there's no one present to confirm a prompt.
Stated once so the rest of this section can be read in context:
- What kritya is, under the Act. kritya is a provider of an AI system — an agent that reads and writes files and runs shell commands through a model the user configures. It is not a provider of a general-purpose AI model (Title VIII / Chapter V obligations attach to the model provider, not to us).
- Risk classification. kritya is not a high-risk system. It is a developer tool and none of the Annex III categories (employment, education, credit, critical infrastructure, law enforcement, migration, justice, biometrics) describe it. It is also not a prohibited practice under Article 5 — it does not manipulate behaviour, exploit vulnerability, or perform social scoring or real-time remote biometric identification.
- What therefore applies. The transparency obligations of Article 50, and nothing else in the Act's substantive requirements. Chapter III duties (risk management, technical documentation, QMS, conformity assessment, registration) are high-risk obligations and do not apply to kritya. We deliberately do not fabricate them.
kritya is an AI agent: you launch it by name and it reads/writes files and
runs shell commands via a model you configure, so there is no disguise to
disclose — Article 50(1)'s "obvious from the circumstances" exemption
applies. On first interactive run in a workspace, kritya prints a one-time
notice to that effect, recording the acknowledgment (with a timestamp, not
just a boolean) in ~/.kritya/ai-disclosure.json, keyed by workspace path —
so it shows again in a new project rather than being silently suppressed
machine-wide. By default, /commit appends a
Generated-By: kritya (<provider>/<model>) trailer to the commit message it
writes, disclosing that the change was AI-assisted; set commitAttribution
to false in config to opt out.
Headless/CI mode. kritya --prompt never shows the interactive notice —
this is a deliberate scope decision, not an oversight. There is no TTY to
show a notice to and no human present to read one; the workspace's own
.kritya/settings.json allow rules, hooks, .env, and custom commands stay
inert there unless --trust is passed explicitly (see "Scope and design
notes" above), and that explicit flag is itself the informed, deliberate
invocation Article 50(1)'s "obvious from the circumstances" exemption turns
on.
Known position: kritya does not embed machine-readable provenance markers in the content it generates, and we have decided not to add them at this time.
Article 50(2) requires providers of AI systems that generate synthetic audio, image, video, or text to mark the output in a machine-readable format so it can be detected as artificially generated. Our reasoning for the position above:
- Code is not the media 50(2) targets. The obligation is drafted around synthetic audio, image, video, and text published to inform the public. Code written into a source file is functional rather than communicative, and the Commission's own implementation work has not settled that code falls within "synthetic text." We do not claim blanket exemption — we state a position and will revisit it as guidance lands.
- A marker would not survive. Any provenance we inserted into a source file is stripped by the first formatter, compiler, bundler, or copy-paste into another file. A marker that disappears on ordinary use is worse than none, because it implies an assurance it cannot keep.
- The disclosure that does survive is the one we already make. The
Generated-By:commit trailer travels with the change in version control, which is where provenance actually matters for code.
We treat this as an open item rather than a settled one: if the Code of Practice on marking synthetic content, or subsequent guidance, brings generated code clearly into scope, this position will be revisited. In the meantime, anyone relying on kritya output in a context that mandates machine-readable provenance should add their own marking step downstream.
Neither capability is present. kritya performs no emotion recognition and no biometric categorisation of any kind: it has no model, classifier, or code path that infers emotional state from a person or classifies an individual against biometric data. The Article 50(3) disclosure duty is therefore not triggered.
To be precise about image input, since kritya does accept it: a user can
attach an image (an @image.png mention) and kritya forwards it to the model
as a base64 image_url content block, for providers that support vision.
Accepting pixels is not the regulated act — 50(3) attaches to the system
performing emotion inference or biometric categorisation, and kritya adds no
such processing of its own. What the configured model does with an attached
image is that provider's responsibility (see "Scope" below), and a user
attaching a picture of a person is making an ordinary tooling choice, not
invoking a biometric system.
This section exists so the point is stated rather than left to inference.
kritya generates no image, audio, or video and does not produce text for the purpose of informing the public on matters of public interest. The 50(4) disclosure duty is not triggered. Where kritya's output is published in such a context, the user doing the publishing carries that obligation, as with any tool.
kritya supports MCP servers and agent plugins, which can add tools that ingest data — including images — from sources kritya itself does not control. Two points matter for the assessment above:
- Each server and plugin is trust-gated individually, and any configuration change counts as new and re-prompts, so nothing of this kind runs without an explicit user decision.
- A third-party tool that performed emotion recognition or biometric categorisation would do so as that third party's system, not kritya's. We do not ship such a tool, and enabling one is the user's decision and the plugin author's responsibility — the same position as any other third-party integration (see "Out of scope" in the threat model).
kritya is not a high-risk system, so Article 14 does not apply to it as a legal matter. The property is nonetheless a deliberate design choice, and it is the strongest safety guarantee in the product: every action that mutates state requires a human decision.
- Writes, edits, and shell commands each prompt before running. Deny rules
always win over allow rules. An "always allow" for a shell command is scoped
to that exact command, not the program name, so approving
git statusin one workspace is not an approval ofgiteverywhere. - The danger detector forces a warning prompt for destructive and irreversible
commands —
rm -rf, force-push,curl | sh, exfiltration patterns,base64/evalobfuscation — even when an allowlist rule would otherwise cover them. Oversight cannot be configured away into a blanket allow. - Subagents that have no user present to confirm a prompt are constrained rather than trusted: read-only subagents are given no write, edit, or shell tool at all, and write-capable ones run isolated on their own git worktree/branch with destructive commands blocked outright.
- Headless mode requires an explicit
--trustopt-in before workspace-local configuration takes effect, so autonomy is never inherited silently. - An OS-enforced sandbox (
bwrap/sandbox-exec) confines writes to the workspace as a backstop behind all of the above, and/doctorruns a live canary proving containment actually holds on the current machine.
kritya's own disclosure covers kritya as the AI system under Article 50(1) — it does not, and cannot, discharge whatever separate obligations your chosen model provider (NVIDIA, OpenAI, Anthropic, etc.) has as a GPAI model provider under Title VIII. Those are a different party's responsibility under the Act.
kritya collects no telemetry by default. It talks only to the model
provider you configure (and to Tavily if you use web search). The one opt-in
exception is KRITYA_OTEL_ENDPOINT: if explicitly set, tracing/metrics spans
are exported to the OpenTelemetry Collector endpoint you configure — nothing
leaves the machine unless you set that yourself. See the README's
Privacy section,
Audit log & telemetry, and
docs/observability.md for the local collector setup.
Everything kritya keeps — config, session transcripts, the audit log, MCP
OAuth tokens, and trust manifests — lives under ~/.kritya/.
- Those files are created
0600inside0700directories. Because POSIX mode bits are a no-op on NTFS, on Windows kritya additionally strips inherited ACEs from~/.kritya/and grants full control to only the current user and SYSTEM, so the same owner-only isolation applies there. - Session transcripts, audit logs, and telemetry files are auto-deleted after
15 days by default (
retentionDaysin config, orKRITYA_RETENTION_DAYS). Set it to0to keep everything indefinitely. See Audit log & telemetry.
Note that the audit log is a local, user-owned record rather than telemetry — it is never transmitted anywhere.