Security

Trust profiles, stop patterns and the command log. What limits an agent, and how to check it.

An agent runs commands on a real machine. The question is not whether you trust it, but what happens the one time it gets something wrong. This page describes what limits it.

Trust profile

A trust profile is a set of rules defining what an agent may do on its own, what needs approval, and what it may not do at all. A profile is defined at the workspace level and assigned to a project.

There are three built-in profiles:

ProfileWhat it is for
FullThe agent runs commands itself. For sandboxes and personal projects.
CautiousOrdinary commands by itself, dangerous ones through approval. A sensible default.
Everything on approvalEvery command needs permission. For getting acquainted, and for machines that aren't yours.

Your own profiles are built from these: a list of what is allowed, a list of what is forbidden, and a list of what requires approval. Custom profiles are available from the Company plan up. The three system ones work on every plan, the free one included: safety does not switch off with the payment.

Stop patterns

Separately from profiles there are rules that stop a command by its shape. They exist precisely because a "dangerous command" rarely looks obvious.

What is covered specifically, rather than in general:

  • deleting at the root: rm -rf / and its variants with globs;
  • going around it with sudo: sudo rm -rf / does not get safer for starting with a different word;
  • going around it with ssh: ssh host 'rm -rf /' runs elsewhere, but it runs;
  • going around it with docker exec: a command inside a container is still a command;
  • disk operations, edits to system settings, powering the machine off.

The parsing follows the meaning of the command, not a substring: a command with a harmless beginning and a dangerous continuation does not get through.

The rules are closed by default

If a rule cannot parse a command, that command is not treated as safe. An unclear command is stopped, not waved through. This is a deliberate choice in favour of one extra question over one missed disaster.

A lower bound, not a sandbox

Stop patterns catch the commands that go wrong most often, and they read a command the way it is written. They set the lower bound of protection, not a wall around the agent. A trust profile is insurance against a mistake, not a defence against hostile input: for content you do not trust the barrier is a container.

Exceptions to approval

A profile can name commands that go through without asking, even when the profile otherwise requires approval. In the editor this is the "Allowed commands" field. An exception is a regular expression, and it is the weakest rule in a profile, so it works narrowly:

  • It is written with ^ and compared from the start of the command. ^git\s+status and ^ls\b let git status and ls -la through. The platform refuses to save an exception that does not begin with ^ and shows how to fix it. Profiles saved earlier keep working and are read the same way: the ^ is required when their rules are saved again.
  • It applies to a single command only. A chain with ;, && or |, a redirect into a file with >, a VAR=value before the command, and options that write or run something (find -delete, find -exec, rg --pre, git diff --output, sed -i) take a command out of the exception, and the profile's general rule decides. Redirects to /dev/null and 2>&1 do not count.
  • A stop pattern wins. A command that matches a stop pattern, or breaks the profile's git and network rules, is stopped even if an exception fits it too.

Testing the rules before you need them

The profile editor has a field where you can type a command and see what would happen to it: allowed, forbidden, or requiring approval.

Use it while writing your own profile. Testing your rules on a real task is a poor way to discover a typo in them.

Protected paths

Paths the agent may not touch even when the command itself is allowed come in two kinds.

Built in, under every profile. The files that configure the agent and its hook are always protected, Full included. Writing to them needs a person's approval: ~/.quiel, .quiel.json, .claude/settings*.json, .claude/hooks, .mcp.json, .codex, .cursor, OpenCode's settings and plugin, .git/hooks and .git/config. This is the one thing an agent must not be able to do for itself: relax its own rules or forge an approval by editing a file. The set holds at any depth, so a settings folder inside a monorepo subfolder or a task's working copy is covered too. It cannot be turned off.

Your own. Directories with keys, system settings, whatever you decide, set in the profile. Paths are counted from the repository root, not from the directory a command happens to run in, so the protection holds when the agent works from a subfolder. Paths are compared without regard to letter case: writing to .ENV or Infra/main.tf needs approval just like .env and infra/. On macOS and Windows that is the same file; on Linux the price is an extra question.

Protection follows what a command does, not the tool it uses. It covers Edit and Write, and just as much MultiEdit, NotebookEdit, apply_patch (Codex and OpenCode) and Bash commands that write to files: redirects, tee, sed -i, cp, mv, rm and the like. File tools are covered on Claude Code, Codex and OpenCode; Cursor's hook sees only shell commands, so that part is not available there yet: see the table on Agents.

Outside the project. When a file tool writes outside the project, the decision is left to the agent itself, through its own permission prompt: Quiel neither allows it on the agent's behalf nor forbids it. The system temporary directory is the exception: there the profile decides, as inside the project. Shell commands are judged by the profile's command rules, as before.

Approvals

When a command needs a person's decision, the agent asks for an approval, and the request appears on the project's "Approvals" screen (see Collaboration). Three properties matter for safety:

  • An approval is tied to the exact command and directory. Allowing rm -rf build in one directory does not allow the same line in another, nor a different command that merely looks similar. Both parts are compared in full when the approval is used. A request whose directory is not an absolute path, or whose command or directory contains a null character, is refused.
  • A file-write approval covers exactly the content that was requested. The approval is tied to a fingerprint of the whole tool call, the path and the content together, so any other content for the same path needs a new approval. The content itself never reaches the platform: it is the same rule as for the rest of your code. The request shows the path, the fingerprint and call statistics (how many files, lines added and removed, and bytes). The client builds the statistics from the real tool call, not the agent in its own words. What is actually being written can be checked in the agent's terminal. In Telegram such a request gets buttons only if the statistics fit in the message. This needs @quiel/cli 0.13.0 or newer; restart an agent session that was started before the update before you approve a write.
  • Telegram shows the buttons only when you can see everything you are allowing. "Allow" and "Decline" appear in the message only if the whole command and the directory fit in it as they are. If the command is too long for a message, hides a secret that had to be masked, or contains invisible or control characters, the message says why and links to the approval on the site. There the command is shown in full and the decision is made. In messages sent before this rule, "Allow" no longer works: the bot sends you to the site, while "Decline" still works.

The command log

Every command an agent runs is recorded: what was executed, in which task, by which agent, and how it ended.

The log is visible on the project's "Commands" screen and on a tab inside the task. There are filters, a live tail and a CSV export.

Four properties worth knowing:

  • Secrets are masked. Tokens and keys inside commands are replaced with a marker, so the log does not become the place where they leak.
  • Entries cannot be changed or deleted. That is not a promise made in the interface but a restriction at the level of database privileges: the application physically has no right to update or delete log rows. Even a bug in the platform's code will not rewrite history.
  • The plan limits how far back you can look, not how long we keep it. Lower plans show the recent weeks, higher ones show everything. Every entry is kept regardless: as said above, the platform physically cannot delete them. Move to a higher plan and the earlier history is there.
  • Recording has a rate ceiling. For one agent the platform accepts up to 1,200 commands a minute and 150,000 a day; beyond that, events are dropped rather than recorded. An event with a command longer than 131,072 characters is dropped whole. A working agent never gets near these limits; details on the Agents page.

Agent tokens

A token is what lets an agent connect to the platform. Revoking it takes effect at once: an agent that is connected right now is cut off too, with no waiting for it to reconnect. The "Revoke token" button is in the agent's row on the project's Agents page. A revoked token cannot be revived: the agent needs a new one. What happens when a person is removed from a project or loses a role is on the Agents page.

The token goes only to its own platform. .quiel.json lives in the repository and can come from someone else, and the platform address in it can be forged. So the client sends the token only to the address it was issued for, saved next to it in ~/.quiel. If .quiel.json points elsewhere, the client sends nothing and suggests running quiel init. The agent id in .quiel.json may contain only letters, digits, - and _ (up to 128 characters), and the client's state files are only ever kept inside ~/.quiel.

Signing in with a code

A sign-in code is six digits, and six digits can be guessed, so it is guarded by limits:

  • three tries per code; after that the code is no longer accepted and you need a new email;
  • 20 tries a day per address; a new email does not reset that count, otherwise guessing would simply go on at three tries per email;
  • every try counts, however the code is submitted: from the sign-in page or by a direct request. There is no path around the counter.

Sign-in emails are limited too, so that nobody's mailbox can be flooded:

  • per mailbox: no more than once a minute and ten an hour. A mailbox is counted by what it is, not by how it is spelled. On Gmail a +label and dots in the name change nothing, so [email protected] and [email protected] are the same mailbox as [email protected]; on Outlook, iCloud, Proton and other services that ignore a +label, the label is folded away (dots matter there). Other domains are counted as written;
  • per client: no more than 60 emails an hour (a client is told by its network address). That is a ceiling against trying thousands of different addresses; an honest person needs one email;
  • the slot is taken before the email is sent, in a single operation, so a hundred parallel requests cannot get past the limit.

If the counters are temporarily unavailable, emails still go out (otherwise nobody could sign in) and signing in with a code is closed.

The link and the code work where you asked to sign in. The browser in which you requested the email gets a mark; without it the code is not accepted and the link does not sign you in straight away. Otherwise someone could request an email for their own address and pass the link to another person: that person's browser would silently sign in to someone else's account, and whatever they typed next would go to the sender.

  • Enter the code in the same browser where you requested the email. The usual case works as before: you ask to sign in on your laptop, open the email on your phone and type the code on the laptop. A code from an email requested in another browser will not work; request a new email here.
  • A link opened in the same browser signs you in at once. In another browser or on another device you see a "Sign in as …?" page with the account address, and you sign in with a button. If another account is already signed in in that browser, the page warns that you will be signed out of it.
  • Until you press the button the link is not used up, so mail scanners that open links in advance no longer burn your sign-in.

Sessions are stored as hashes. The session cookie value exists only in your browser; the database and its backups hold a hash of it, which cannot be used to sign in. Quiel does not keep the GitHub and Google tokens received at sign-in.

Images in text

Text in Quiel (descriptions, comments, results, questions, documents) is written in Markdown, and part of it comes from agents and other people. An image in such text is not loaded by the browser on its own: automatic loading would tell the author that you opened the screen, when and from which address, and would let the image address carry out what the agent saw while working (?d=…). Instead you get a link that you open yourself. If an image has nowhere to lead (a path inside the site, an address like //host, data:), only its caption remains.

What the platform knows about your code

The client never sends the contents of your project files to the server, only names, paths and commands. This is not a setting you can switch on: the client has no upload code for file contents at all, and that absence is the guarantee.

There is a direct consequence for project documents. A specification often lives in the repository itself: docs/SPEC.md next to the code, versioned with it. Such a document is added by path: in the "Add document" dialog pick "Path in repository" and type docs/SPEC.md.

The platform stores the address, not the text. The agent receives the same path and opens the file on its own machine, since it is already on disk there. Such a document cannot be downloaded through the platform: there is nothing to download.

If the contents must be visible in the browser to people who do not have the repository, the two earlier ways remain: text written in the platform and an uploaded file. The choice is yours, and it is a deliberate one: visibility in exchange for a copy.

What stays on your side

Plainly about the limits:

  • The agent works with the privileges of whoever started it. If that person has access to the production server, so does the agent on their machine. The platform restricts commands; it does not rewrite operating-system permissions.
  • A profile protects against a mistake, not against ill intent. The person who started the agent can run the same commands by hand: the log will see it, but it will not stop them.
  • Untrusted content calls for a container. If an agent works on text you do not control (a task that came from outside, someone else's repository, an attachment), run that agent in a container: no profile can promise to recognise every spelling of a harmful command.
  • The keys stay with you. The platform does not store your SSH keys or your server access tokens.

On this page