Skip to content

Getting started

This page assumes you’ve already installed Seal and exported a CLAUDE_CODE_OAUTH_TOKEN (the least-friction setup — see Install: Set up an LLM provider or Provider backends for other options).

Terminal window
cd ~/code/my-project
seal embark

seal embark is the first-run wizard. It confirms a credential is set, walks you through provider selection, and stamps a starter seal.toml into the project root. The starter is intentionally minimal — it grants read access to common source-file types, a handful of safe build / git commands, and the default Anthropic provider.

For subsequent projects (or to re-scaffold a manifest in a project where credentials are already configured), seal init does the same scaffold without the credential walk-through.

Open seal.toml and look at it. The shape is:

schema_version = 1
[model]
provider = "anthropic"
name = "claude-sonnet-4-6"
[capabilities.allow.read]
default_files = ["*.rs", "*.toml", "*.md"]
paths = [
"src/**",
"tests/**",
"crates/**",
{ path = ".", files = ["Cargo.toml", "Cargo.lock", "README.md"] },
]
[capabilities.allow.write]
default_files = ["*.rs"]
paths = ["src/**", "tests/**", "crates/**"]
[capabilities.allow.commands]
patterns = [
"cargo test:*",
"cargo check:*",
"cargo fmt:*",
"cargo clippy:*",
]
[capabilities.deny.read]
default_files = ["*.key", "*.pem", "*.seed", ".env", ".env.*"]
[sandbox.os]
command_fs = "system"
command_tools = ["cargo"]

The exact set of grants depends on the project type embark detected (Rust, Node, Go, Python, generic) — Rust shown above. Either way the starter is intentionally narrow: a few source-file types under the standard project paths, a small allowlist of read-only build commands, and an explicit deny on credential-ish files. Everything else surfaces as a prompt the first time the agent tries it.

Terminal window
seal

This launches the TUI. The first time it runs in a project you’ll see a “manifest approval” prompt — Seal hashes the on-disk seal.toml and asks you to confirm. Press y to accept; the session starts.

If you’ve already approved the same seal.toml content before (the hash matches the last-signed value), the approval prompt is skipped and the session opens directly into the composer.

Type into the composer at the bottom of the screen. A starter prompt to verify everything works:

Can you read README.md and tell me what this project does?

What you’ll see:

  • The agent calls file_read against README.md. That matches the read grants — no prompt fires.
  • The agent issues a response based on the file content.

A more interesting prompt:

Add a TODO comment at the top of src/main.rs reminding me to add tests.
  • The agent calls file_read against src/main.rs (read grant matches, silent).
  • The agent calls file_edit — which writes — against src/main.rs. The starter manifest’s write grant covers **/*.rs, so no prompt.
  • The edit lands; the diff renders inline.

Now try something the starter doesn’t grant:

Run `cargo nextest run` and show me which tests are failing.
  • The agent calls command_run with cargo nextest run.
  • That pattern isn’t in [capabilities.allow.commands.patterns].
  • A permission prompt appears with the suggested pattern cargo nextest run:* at the top and four choices: Allow / Allow always / Deny / Deny always.

The suggested pattern matters as much as the choice. The arrow keys walk it across scale levels:

cargo:*
cargo nextest:*
cargo nextest run:*
cargo nextest run
  • Right narrows: more literal tokens, fewer matches.
  • Left broadens: fewer literal tokens, more matches.
  • Tab snaps back to the daemon’s recommended level.

Two distinct moves, picked deliberately:

  • Narrow + Allow always — for this specific call, pick the level you trust (cargo nextest run:* or cargo nextest:*). That lands a silent-allow pattern for the chosen scope; future matching invocations run without asking.
  • Broad + Allow — broaden the pattern (cargo:*) and pick Allow (not Allow always). That writes the broad pattern as prompt = true — the call runs once now, and every future cargo subcommand prompts again at its own specific level, where you can narrow + Allow always on the subcommands you decide to silently allow.

The second move sets up the layered shape the next section covers: one broad prompt = true and several narrow silent allows on top. Don’t broaden + Allow always — that hands the whole tool family a silent pass, including subcommands you’ve never seen. cargo nextest:* is fine to silently allow; cargo:* covers cargo install and cargo publish too and is too wide for an unattended grant.

See Permission model for the full four-way semantics and Specificity scores for exactly how the matcher picks between layered patterns.

After a few sessions of the step-4 flow, [capabilities.allow.commands] ends up shaped like one broad prompt = true per tool family with narrow silent allows on top:

[capabilities.allow.commands]
patterns = [
{ command = "git:*", prompt = true }, # ask before any git operation by default
"git status", # except these read-only ones
"git diff:*",
"git log:*",
"git branch:*",
]

Most-specific-wins matching handles the layering: git status matches both entries, the narrower one wins, the call runs silently. git reset --hard HEAD~3 only matches the broad prompt, so it prompts — same for git push --force, git clean -fdx, and anything else under git:* that you haven’t pre-cleared.

You can also anticipate this shape instead of discovering it call-by-call. Open seal.toml and write the broad prompt = true plus the narrow silent allows you already know are safe. The first session after the edit re-validates the manifest signature, you re-sign on the approval modal, and the layered shape is live from the next call. The /reload slash command does the same in-session without a relaunch.

The seal project’s own seal.tomlgithub.com/sealedsecurity/seal/blob/main/seal.toml — uses this pattern across git, cargo, gh, just, and jj. Read it for a real-world layered manifest after a few dozen sessions.

Filesystem grants work differently from command grants — they’re a two-list combination, not a single pattern. Worth understanding because the prompt UX is shaped by it.

[capabilities.allow.read] (and .write, .delete) takes a default_files glob list and a paths list. The two compose: each bare-string paths entry inherits default_files, producing the cartesian product. From the starter above:

[capabilities.allow.read]
default_files = ["*.rs", "*.toml", "*.md"]
paths = ["src/**", "tests/**", "crates/**"]

This grants reads of src/**/*.rs, src/**/*.toml, src/**/*.md, tests/**/*.rs, etc. — every combination. To grant a path with a different file-type list, use the table form:

[capabilities.allow.read]
default_files = ["*.rs", "*.toml", "*.md"]
paths = [
"src/**", # inherits default_files
{ path = ".", files = ["Cargo.toml", "Cargo.lock"] }, # overrides default_files
]

The table form’s files list replaces default_files for that entry; it doesn’t extend it.

Globs use a standard set of operators:

  • * — anything except a path separator (matches foo.rs, not dir/foo.rs).
  • ** — anything including path separators (src/** matches src/foo.rs and src/a/b/c.rs).
  • ? — single character.
  • {a,b} — alternation.
  • [abc] — character class.

The same engine powers default_files, paths, the deny side, env-var passthroughs, and command-pattern colons — so the syntax is consistent across the manifest.

[capabilities.deny.*] always wins. A bare default_files under a deny section means “these file types anywhere in the project” — the daemon injects an implicit ** path:

[capabilities.deny.read]
default_files = ["*.key", "*.pem", ".env", ".env.*"]

This blocks .env at the project root, app/.env.local, ~/.ssh/id_rsa (if ~/.ssh were otherwise readable), and so on. There’s no allow grant that can override a matching deny.

To grant access to paths outside the project root — a sibling repo, a temp dir, ~/.config/<tool> — list them under [capabilities.allow] additional_directories:

[capabilities.allow]
additional_directories = ["~/code/shared-lib", "/tmp/seal-scratch"]

Two important things:

  • additional_directories does not grant any reads or writes on its own. It just expands the universe of paths your paths entries are allowed to reference. To actually grant access, a [capabilities.allow.read] or .write entry has to name a path inside one of the listed directories.

  • No automatic glob expansion. Listing ~/code/shared-lib does not implicitly allow ~/code/shared-lib/**. You still write the explicit paths entry:

    [capabilities.allow]
    additional_directories = ["~/code/shared-lib"]
    [capabilities.allow.read]
    default_files = ["*.rs"]
    paths = ["src/**", "~/code/shared-lib/src/**"]

additional_directories also widens the cwd allowlist for unscoped command grants — a bare "cargo build:*" matches when run inside any listed directory, not just inside project root.

Manifest-level reference: capabilities.allow. Real-world example: the seal project’s own seal.toml shows the layered grants, default_files × paths shape, and additional_directories usage on a non-trivial project.