{"slug": "can-you-say-what-your-ai-written-tasks-can-access-before-you-run-them", "title": "Can You Say What Your AI-Written Tasks Can Access Before You Run Them?", "summary": "A developer built Lask, a task runner in which tasks are written as typed functions in a small language and every command runs inside a container of a declared Docker image. Version 0.15.0 adds a `within` feature that lets users declare which files and networks commands may access, with the type checker statically verifying that every command stays within that scope so violations fail `lask check` before anything runs. The developer frames the work as boundary verification around AI-generated code, citing the xz Utils backdoor and studies showing code review is falling behind as AI increases output.", "body_md": "Ask a coding agent to add a task, and the diff fills up with shell commands. Few people review them down to what `npm install` runs underneath.\n\nWhat can those commands overwrite, and what can they talk to? I wanted a machine to answer that before anything runs.\n\nI build [Lask](https://github.com/lask-task-runner/lask), a task runner where you write tasks as typed functions in a small language, and every command runs in a container of the Docker image you declared for it.\n\nLask 0.15.0 adds `within`: you declare which files and networks commands may access, and the type checker verifies it statically.\n\nThis post has two parts. The first part steps away from Lask and summarizes the recent discussion about guardrails for generative AI. I tried to keep it objective, and I mark where I give my own opinion. The second part shows how I implemented those ideas in Lask, so it is mostly my own take.\n\n**TL;DR**\n\n**Part 1 (general)**\n\n- Reading code was never a strong defense against malicious code, and with AI producing more code, review is falling further behind.\n- One response is to have AI prove its code correct. But proofs need a specification written by a human, which doesn't fit things like the install scripts\n`npm install` runs.- Another response, with growing research behind it, is to verify the boundary around the model, which holds whatever the model outputs.\n**Part 2 (Lask)**\n\n- Lask's\n`within` implements that second idea as type checking in a task runner. Commands that might exceed the declared scope fail `lask check` before anything runs.- I suggest a split: a person checks the access scope with\n`lask envs access` before running, and `lask check` verifies that every command stays within it. This is static checking, not a machine-checked proof, and there are things it doesn't cover. They're listed near the end.\n\nEven when people wrote all the code, finding malicious code by reading it was hard.\n\nThe xz Utils backdoor, found in March 2024, wasn't caught by reading code ([XZ Utils backdoor](https://en.wikipedia.org/wiki/XZ_Utils_backdoor)). Andres Freund, a PostgreSQL developer, traced it while investigating unusually high CPU usage during SSH logins.\n\nDependency code often isn't read at all. Imtiaz and Williams studied updates to popular packages in four package registries ([arXiv:2206.09422](https://arxiv.org/abs/2206.09422)). 52.5% of updates were only partially code-reviewed, and only 9.0% of packages had every update fully reviewed.\n\nThe report from the S3C2 industry summit on supply chain security reaches the same view ([arXiv:2505.10538](https://arxiv.org/abs/2505.10538)). Since there is no reliable automated way to detect malicious commits, teams rely on human review, but human review is error-prone and not always reliable.\n\nWith AI writing code, there's more for people to read.\n\nChen and Stratton at Harvard analyzed development data from 655 firms ([Artificial Intelligence in the Firm](https://fion.ac/jellyfish.pdf)). It's a working paper estimating how AI coding tools affect development, and it hasn't been peer-reviewed. After firms adopted AI agents, lines of code added rose 44.0% and pull requests opened rose 35.7%. Meanwhile, the average time from opening a pull request to merging it grew 56.1%. The authors conclude that code review became the bottleneck.\n\nHe et al. followed 802 developers at a company that mandated doubling output with AI ([arXiv:2607.01904](https://arxiv.org/abs/2607.01904)). Load per reviewer roughly doubled.\n\nReading code was already a weak defense against malicious code, and AI leaves even less room to read. So I think we need checks that a machine can do without a person reading the code.\n\nIn December 2025, Martin Kleppmann predicted that AI will make formal verification mainstream ([AI will make formal verification go mainstream](https://martin.kleppmann.com/2025/12/08/ai-formal-verification.html)). Formal verification proves mathematically that a program meets its specification. His argument: AI makes writing proofs much cheaper, so instead of having people review AI-written code, have the AI prove it correct.\n\nWith this approach, it doesn't matter if the AI writes a bad proof. The proof checker rejects it and makes the AI try again.\n\nBut in the same post, Kleppmann notes that reading and writing formal specifications still takes expertise and careful thought. A proof only guarantees that the code meets the spec, and a person writes the spec.\n\nI think proof-based verification is hard to apply to tasks like builds and installs. `npm install` runs install scripts written by other people, one for each dependency that has one. Nowhere is there a spec for what those scripts should do.\n\nThis year, those install scripts were exactly what attackers used on npm. Palo Alto Networks' Unit 42 updated its overview of attacks on npm on July 15, 2026 ([The npm Threat Landscape](https://unit42.paloaltonetworks.com/monitoring-npm-supply-chain-attacks/)). It says many compromises rely on preinstall or postinstall hooks to exfiltrate secrets.\n\nAccording to the same article, in the May TanStack incident, code injected into CI harvested `NPM_TOKEN` and `GITHUB_TOKEN`. In April, a self-propagating worm was reported that used stolen npm tokens to publish versions carrying malicious code ([The Hacker News](https://thehackernews.com/2026/04/self-propagating-supply-chain-worm.html)).\n\nThe common advice is `ignore-scripts`, which stops install scripts. But some packages need their scripts, so stopping all of them isn't always practical.\n\nInstead of checking that AI output is correct, a growing body of research checks the boundary that AI output can't cross, whatever it is.\n\nMoon and Varshney's Containment Verification ([arXiv:2605.09045](https://arxiv.org/abs/2605.09045)) assumes nothing about how the AI behaves. It treats the AI as able to emit any value within the types the framework accepts. It then proves in Dafny that the agent framework enforces its boundary policy for every such value. The authors say the guarantee doesn't depend on alignment, that is, on whether the model has been tuned to behave as intended.\n\nGoogle DeepMind's CaMeL ([arXiv:2503.18813](https://arxiv.org/abs/2503.18813)) also leaves the model untouched. It places an interpreter outside the model that records where each value came from and checks those records against a policy before any tool call, to defend against prompt injection.\n\nAnthropic's Agent SDK documentation draws the same line between permission checks and isolation ([Securely deploying AI agents](https://code.claude.com/docs/en/agent-sdk/secure-deployment)). Of the mechanism that parses commands and matches them against permission rules, it says: \"This is a permission gate, not a sandbox.\"\n\nPutting this together, the mechanisms that guard against an agent's risky operations fall into three layers. Each works at a different time and checks something different. Table 1 lists them in the order they act.\n\nTable 1. Three layers against risky operations\n\n| Layer | What it checks | When | If it gets it wrong | \n|---|---|---|---|\n| Static boundary check | Whether the isolation mechanism and its settings keep to the decided scope | Before running | It runs with isolation that doesn't keep to the scope | \n| Permission check | Whether this operation may run | Right before running | The operation runs | \n| Runtime isolation | What a running operation can access | While running | It accesses more than intended | \n\nA static boundary check examines the isolation mechanism, such as a container or sandbox, and its settings before anything runs. Containment Verification, from the previous section, proves that the framework enforces its boundary policy whatever values the AI emits. It checks the isolation, not the AI's output, so the result doesn't change whatever the AI writes.\n\nA permission check decides, right before running, whether an operation runs. Permission rules and confirmation prompts belong here. If the check gets it wrong, the operation simply runs.\n\nRuntime isolation has no say in whether something runs. The operation runs, but it can only access what's inside the isolation. Even a risky operation that got past the permission check is contained. But if the isolation is configured too broadly by mistake, it can access more than intended. Without a static check, nobody usually verifies those settings until something runs.\n\nThe three layers are meant to be stacked. The static check catches mistakes in the isolation settings before anything runs, and isolation contains risky operations that got past the permission check.\n\nFrom here on, it's about Lask. I implemented the static boundary check, the first layer in Table 1, as type checking in a task runner.\n\nLask has always run every command in a container. The host's home directory and `~/.ssh` aren't visible from inside.\n\nBut it mounted the repository directory read-write at `/work`. An install script could overwrite source files, read `.env`, and talk to the outside world.\n\nWith `base_access`, you can narrow that mount path by path. The example below makes the whole repository read-only, lets only `dist` be written, and makes `.env` appear empty. `network: \"none\"` also cuts off outside access.\n\n```\noffline = #node:24.21.0-alpine3.24{base_access: { \".\": \"ro\", \"dist\": \"rw\", \".env\": \"none\" }, network: \"none\"}\n```\n\nIn a container with these settings, I tried writing to the source, reading `.env`, and reaching the npm registry.\n\n``` bash\n$ touch src/x.js\ntouch: src/x.js: Read-only file system\n\n$ wc -c .env\n0 .env\n\n$ node -e \"fetch('https://registry.npmjs.org')...\"\nTypeError: fetch failed\n  [cause]: Error: getaddrinfo EAI_AGAIN registry.npmjs.org\n```\n\n`.env` is 11 bytes on the host but 0 bytes inside the container. I checked this with Lask 0.15.0, Docker Engine 29.5.3, and macOS 26.6.2 on Apple Silicon.\n\nSo far, you could do the same by assembling Docker's `--mount` and `--network none` yourself. The difference is the check in the next section.\n\n`within` declares what a module or function may access. Written at the top of a module, it covers every declaration in the module.\n\n```\n// main.lask\nwithin { files: { \".\": \"ro\", \"dist\": \"rw\", \".env\": \"none\" }, network: \"none\", volumes: false, publish: false }\n\noffline = #node:24.21.0-alpine3.24{base_access: { \".\": \"ro\", \"dist\": \"rw\", \".env\": \"none\" }, network: \"none\"}\n\n// Builds into dist without any network.\nbuild(): String = do {\n  $[offline] npm run build\n  read_file(\"dist/manifest.json\", offline)\n}\n```\n\nIt can restrict four things:\n\n`files`: what can be read or written, path by path, inside the repository`volumes`: whether host paths outside the repository can be mounted` network`: whether the container can talk to the outside` publish`: whether the outside can connect to the container\n`lask check` follows every place in the module that runs a command, and verifies that the settings of the container running it stay within the declaration.\n\nNow suppose an agent adds a task that uploads `dist` to a preview server. It needs the network, so the agent also adds `within { network: \"default\" }` to the function.\n\n```\n// main.lask (added)\n// Uploads dist to the preview server.\nupload() within { network: \"default\" } = do {\n  online = #node:24.21.0-alpine3.24{base_access: { \".\": \"ro\", \"dist\": \"rw\", \".env\": \"none\" }}\n  $[online] npx -y some-uploader dist\n}\n```\n\nHere's what `lask check` says:\n\n``` bash\n$ lask check\nmain.lask:14:3-14:38: E-TYPE-WITHIN-EXCEEDED [static]: the network is \"default\", which within { files: { \".\": \"ro\", \".env\": \"none\", \"dist\": \"rw\" }, volumes: false, network: \"none\", publish: false } of the module does not allow (network)\n  note: the constraint is at main.lask:1\n  note: reached through upload, at main.lask:14\n```\n\nThe module's `within` acts as a ceiling, so a function's `within` can't widen it. A function's constraint is checked together with the module's, and both must hold.\n\n`lask run` runs the same check first, so not a single task in this module runs.\n\nIf a network setting is read from an environment variable, its value isn't known until the program runs. Lask treats that as \"can't be shown to hold\" and reports an error.\n\n```\nwithin { network: \"none\" }\n\nprobe(): String =\n  $[runnable(#node:24.21.0-alpine3.24, network = get_env(\"NET\"))] node --version\nbash\n$ lask check\np.lask:4:3-4:81: E-TYPE-WITHIN-UNPROVEN [static]: 'network' is not known here without running the program, so within { network: \"none\" } of the module cannot be shown to hold\n```\n\n`E-TYPE-WITHIN-EXCEEDED` means the scope is shown to be exceeded. `E-TYPE-WITHIN-UNPROVEN` means it couldn't be shown not to be. To pass, settings that affect the scope must be literals or names bound to literals. That's less convenient, but once you declare a constraint, I decided only what is shown to stay within it should pass.\n\nWhen a function takes an image and its settings as an argument (an \"environment\" in Lask), the check happens at each call site. For a function you invoke directly with `lask run`, it uses the arguments from the command line and checks before running.\n\n`#local`, which runs commands directly on the host, can access the whole host, so it satisfies no constraint.\n\n```\nl.lask:3:20-3:38: E-TYPE-WITHIN-EXCEEDED [static]: #local reaches the whole host, which within { network: \"none\" } of the module does not allow (network)\n```\n\nInstalling dependencies needs the network. So I put installation in a separate module that allows network access but can only write `node_modules`.\n\n```\n// deps.lask\nwithin { files: { \".\": \"ro\", \"node_modules\": \"rw\", \".env\": \"none\" }, volumes: false, publish: false }\n\ndeps = #node:24.21.0-alpine3.24{base_access: { \".\": \"ro\", \"node_modules\": \"rw\", \".env\": \"none\" }}\n\n// Installs dependencies; install scripts can reach the network but write only node_modules.\nexport install() = $[deps] npm install --no-package-lock\n```\n\nInstall scripts can reach the outside, but they can't read `.env` or overwrite source files. The build side can read `node_modules` but can't reach the network.\n\n*Figure 1. What each task's container can access*\n\n`lask envs access` lists what the commands in a module can access. It runs nothing and doesn't contact the container engine.\n\n``` bash\n$ lask envs access --module deps.lask\nENVIRONMENT                            FILES                             NETWORK    CHECK  REQUIRED BY\n#node:24.21.0-alpine3.24{base_access}  . ro, .env none, node_modules rw  default !  ok     deps.lask: install\n\n1 row (1 registry): 1 ok\n```\n\n`!` marks anything that can access beyond the repository. Here, it shows that the container can reach the outside.\n\nIn Kleppmann's argument, people read the spec and a proof checker verifies that the code meets it. I think Lask can narrow what people read in the same way. Instead of reading every command an agent added, you check the access scope with `lask envs access` before running.\n\n``` bash\n$ lask envs access build\nwithin { files: { \".\": \"ro\", \".env\": \"none\", \"dist\": \"rw\" }, volumes: false, network: \"none\", publish: false }\nENVIRONMENT                                    FILES                     NETWORK  CHECK  REQUIRED BY\n#node:24.21.0-alpine3.24{base_access,network}  . ro, .env none, dist rw  none     ok     main.lask: build\n\n1 row (1 registry): 1 ok\n```\n\nGiven a function, the first line shows the constraint the function is checked against. Below it are the scopes of the commands the function runs.\n\nThe list is built from the same code that runs. If an agent edits the `within` line itself, the constraint on the first line changes, so you notice before running.\n\nWithout a function, you get the list for the whole module. With the `upload()` task from earlier, it looks like this:\n\n``` bash\n$ lask envs access\nENVIRONMENT                                    FILES                     NETWORK    CHECK     REQUIRED BY\n#node:24.21.0-alpine3.24{base_access,network}  . ro, .env none, dist rw  none       ok        main.lask: build\n#node:24.21.0-alpine3.24{base_access}          . ro, .env none, dist rw  default !  exceeded  main.lask: upload\n\n2 rows (2 registry): 1 ok, 1 exceeded\n```\n\nAn `exceeded` row is a task with a command that goes beyond the declaration. Rows marked `!` tell you which tasks talk to the outside. `envs access` runs nothing, and it exits with 0 even when something exceeds the scope.\n\nA person reads this list, and `lask check` verifies that every command stays within it. However many tasks an agent adds, this list is all a person needs to check.\n\nThe split looks like this:\n\n`lask envs access`.` lask run` checks the module before running anything.`ok`, the commands run. If anything is `exceeded` or `unproven`, nothing runs.\n`within` is a static check done by Lask's type checker. Unlike Containment Verification in Part 1, the checker itself isn't machine-verified, for example in Dafny. If the checker has a bug, a command that exceeds the scope can still pass.\n\nWhat it protects is limited too:\n\n`none` or `default`. The install scripts in `deps.lask` can talk to anywhere on the internet. The next section covers limiting destinations.`env` can be read by that command. Pass a secret to a container with network access, and it could still be exfiltrated.`#local` satisfy no constraint. Tasks that need `within`.` lask sync`, which builds and pulls images, isn't covered.` npm install` directly from a shell, Lask can't check it.\nThe last point is a separate problem: how to restrict the agent's own permissions. That belongs to the agent's permission settings and sandbox.\n\nOutbound traffic from containers (egress) is currently all or nothing. The `npm install` in `deps.lask` only needs to reach the npm registry, but with `network: \"default\"`, install scripts can reach anywhere on the internet. As with the npm worms in Part 1, any secret they can read could be sent out.\n\nSo, as the next step, I'm considering egress that lets you list the destinations a task may reach. The design I have in mind:\n\nThe task's container has no way out except through the proxy, so an install script that tries to bypass the proxy can't reach the outside. This is still at the design stage, and the syntax isn't decided.\n\nIt's getting hard to read all the code AI writes. But you can narrow what people read to the list `lask envs access` prints, and leave the rest to type checking. That's what I wanted to show in this post.\n\n`within` just landed in 0.15.0, and both how constraints are written and what the check covers are still being worked out. I'd love to hear how it works for you, or what constraints you'd want.", "url": "https://wpnews.pro/news/can-you-say-what-your-ai-written-tasks-can-access-before-you-run-them", "canonical_source": "https://dev.to/torudev/can-you-say-what-your-ai-written-tasks-can-access-before-you-run-them-2lfj", "published_at": "2026-11-03 14:51:32+00:00", "updated_at": "2026-11-03 14:57:45.662736+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "developer-tools", "ai-tools"], "entities": ["Lask", "Andres Freund", "Martin Kleppmann", "Harvard", "xz Utils"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/can-you-say-what-your-ai-written-tasks-can-access-before-you-run-them", "markdown": "https://wpnews.pro/news/can-you-say-what-your-ai-written-tasks-can-access-before-you-run-them.md", "text": "https://wpnews.pro/news/can-you-say-what-your-ai-written-tasks-can-access-before-you-run-them.txt", "jsonld": "https://wpnews.pro/news/can-you-say-what-your-ai-written-tasks-can-access-before-you-run-them.jsonld"}}