Agent Spec holds your agent's opinions

An agent's opinions, written as a file: its model, window, turns, tools and connections. lnk agent start --spec reads it and lnk agent show --opinions prints the one the agent runs.

Every option

Every key of a spec, by its section, each with a comment saying what it is for.

# The agent itself. Both are yours, to tell your specs apart and to mark a change.
name = "assistant"            # the agent's own name
version = 1                   # a number you bump when you change the file
command_response = "🤖 Automated Response" # label before harness command answers; default ""
# Start from nothing instead of Link's spec, so this file must hold every key but its tools'.
# extends = "none"            # left out, it's "link": Link's spec beneath yours
# extends = "link:coding"     # or "link:personal": a profile of Link's between its spec and yours
# Whose it is and the teams it's on are Link's to say, so a file never claims them.
# owner = "github-1234567"    # printed by `lnk agent show --opinions`; another owner is refused
# teams = ["research"]        # teams whose files list it; any other is refused

# Which harness runs the agent. Switching keeps every opinion below.
[harness]
name = "link"                 # a harness's name: lowercase letters, digits and dashes

# Which model it uses, and how hard it thinks.
[model]
default = "claude-sonnet-5-5" # the model when the agent names none
allow = ["claude-sonnet-5-5", "claude-opus-5-5"]   # the only models it may run; left out, any
answer = 8000                 # the longest answer, in tokens; left out, the window's share
thinking = "medium"           # off, low, medium or high, where the model can think
thinking_budget = 4096        # most tokens it thinks, for a model that takes a budget
fallback = true               # a turn its model can't answer goes to --fallback's model; false, it fails

# A provider's own table takes the same keys as [model] and beats it for that provider.
# The providers are anthropic, openai, openrouter, ollama and local.
[model.anthropic]
default = "claude-opus-5-5"   # the model on Anthropic
thinking = "high"             # think harder on Anthropic only

[model.openai]
default = "gpt-6-luna"        # the model on OpenAI

# How much of the model's window a turn fills.
[window]
max = 200000                  # tokens a turn fills; left out, the model's whole window
unknown = 32768               # the window, in tokens, of a model Link doesn't know
margin = 0.125                # share kept back, since tokens are counted roughly before a turn
answer = 0.25                 # share kept for the answer, where [model] answer doesn't say
compact_at = 0.75             # past this share, the oldest turns leave for a summary; "never": they just leave
save_notes = true             # before it summarizes, it saves what matters to its notes, where it can

# How long a turn may go on, and what a new message does meanwhile.
[turns]
rounds = 8                    # most rounds of tool calls in one turn
calls = 16                    # most calls one answer makes
busy = "steer"                # a message sent mid-answer: queue (after), steer (joins) or interrupt (ends it)
renew = "10s"                 # how often a running turn renews its lease, at least 1s
lapse = "60s"                 # a lease not renewed this long: its turn crashed; over twice renew

# What the agent is told, in sections: Link's, a file in its files, or none.
[instructions]
base = "instructions/base.md"   # in its files: "You are my research assistant. Cite your sources."
environment = "link"          # where its files are, the folders it may change, its connections
skills = "link"               # each skill's name and what it's for, from skills/<name>/SKILL.md
persona = "instructions/persona.md"   # who it is, whatever the task: its name, voice, character
owner = "instructions/owner.md"       # who you are: your name, work, family, how you like things
standing = "instructions/instruction.md"   # your standing instructions, read at each message
now = "link"                  # the date, time, connection and speaker, at the head of each message
notes = "memory/notes.md" # what it noted for itself with memory's remember; "none"
repository = "link"           # the repository's AGENTS.md, else CLAUDE.md, nearest folder first; "none"

# How large each section may be: what's past it is cut.
[instructions.max]
standing = "8 KB"             # 64 B to 64 KB
skills = "8 KB"               # whole skills: those past it are left out
model = "8 KB"                # instructions/models/<maker>/<model>/instruction.md, no source key
repository = "16 KB"          # the repository's AGENTS.md, else CLAUDE.md

[commands]                    # what a message of yours runs, each on when true
help = true                   # /help: the commands that are on
status = true                 # /status: the model and what the conversation holds
create = true                 # /new: a new conversation in the same chat
stop = true                   # /stop: the answer being written stopped
model = true                  # /model: the model, or another of its provider's
models = true                 # /models: the models its provider offers
tasks = true                  # /tasks and /ps: running and scheduled work
write = true                  # /learn: a skill written from what it did, where it may write skills/
skills = true                 # /skill:list, and /skill:<name> runs a skill, yours or the repository's
goal = true                   # /goal: a goal it pursues after each answer, within [goals]
resume = true                 # /chat:resume: the chat's conversations, or one taken up again
fork = true                   # /chat:fork: a conversation holding this one as it stands
rename = true                 # /chat:rename: this conversation's name
export = true                 # /chat:export: this conversation written to a file
queue = true                  # /chat:queue: the next message waits for the running turn
steer = true                  # /chat:steer: the next message joins the running turn
compact = true                # /chat:compact: what's before your last message summarized now
ask = true                    # /chat:ask: a question answered apart, nothing kept
verbose = true                # /chat:verbose: whether this chat shows each tool it calls

# How long /goal pursues a goal on its own turns: whichever comes first.
[goals]
turns = 10                    # turns it takes toward one, after the one that set it
tokens = 1000000              # tokens those turns spend

# What holds for every tool, unless its own table says otherwise.
[tools]
answer = "32 KB"              # most of a tool's answer that's kept, 1 KB to 16 MB
timeout = "5m"                # most a call may take, waiting for you to allow it included
# strangers = ["web__fetch"]  # the tools a stranger's message may use; left out, none

# One tool's own, by its name. Any tool takes answer and timeout; Link's
# tools (memory, scheduler, web, run, file, code) only the keys below, and a tool
# from outside Link keys of its own too, which Link keeps and doesn't read.
# [tool.<name>]
# answer = "64 KB"            # this tool's answer, over [tools]
# timeout = "10m"             # this tool's call, over [tools]

# The memory tool: what the agent remembers and finds again.
[tool.memory]
keep = "10m"                  # how long its session stays once no turn uses it
max_age = "1h"                # how long its session lives at most
tokens = 6000                 # most a search or a read gives back, at least 100
model = "qwen3-embedding"     # embedding model it ranks by; "none": by words only, no model started
notes = "4 KB"                # most its notes may grow to, 64 B to 64 KB: remember refuses past it

# The scheduler tool: the agent's schedules and subagents.
[tool.scheduler]
enabled = false                # default: add scheduler explicitly
per_day = 100                 # most messages its schedules and subagents send it a day, and it takes from each teammate a day (UTC)
subagents = 5                 # most subagents at work at once
hops = 8                      # how deep a chain of messages sent by agents on their own may go
late = "run"                  # a firing missed asleep: run once on waking, or skip
ask = false                   # true: task, schedule and cancel ask you first
zone = "Europe/Paris"         # its times' time zone; "local" is this machine's
firing = "30m"                # how long it waits on a firing before giving up, 1s to 1d
driver = "cron"               # what wakes it: "service", the agent's bridge, "cron", the calendar each minute, or "address", the machine

# The web tool: pages it fetches and searches it makes.
[tool.web]
fetch = "24 KB"               # most of a page one fetch gives, the rest a call away, 1 KB to 1 MB

# The run tool: the programs it runs, which its sandbox file's [commands] names.
# scheduler, run, file and code each take enabled: false, Link doesn't start it.
[tool.run]
enabled = false                # default: add run explicitly
timeout = "2m"                # a command's call; the command stops a tenth before
output = "16 KB"              # what's kept of each of its output and its errors, 1 KB to 16 MB
background = 4                # background commands at once, 0 to 64; 0, none
background_timeout = "1h"     # the longest one runs, 1s to 7d
background_output = "16 MB"   # its output kept on disk, its end, 1 KB to 1 GB
background_keep = "1d"        # its record and output, once it ended and was told, 1m to 365d
background_per_day = 100      # started in a day, each one's ending a turn, 1 to 65535

# The file tool: the files it reads, writes and changes, and finds.
[tool.file]
enabled = false                # default: add file explicitly
find = true                   # files found by name and lines by a pattern; false, no find
diffs = true                  # a write or replace answers with the change as a diff

# The code tool: the language servers it asks, each as its sandbox file's [commands] allows.
# A server waits a tenth less than the call's timeout. Each key of a language is its own:
# naming command alone keeps Link's files and roots. command = [] turns a language off.
[tool.code.servers]
rust = { command = ["rust-analyzer"], files = ["rs"], roots = ["Cargo.toml"] }
go = { command = ["gopls"], files = ["go"], roots = ["go.mod"] }
python = { command = ["pyright-langserver", "--stdio"], files = ["py", "pyi"], roots = ["pyproject.toml", "setup.py", "pyrightconfig.json"] }
typescript = { command = ["typescript-language-server", "--stdio"], files = ["ts", "tsx", "js", "jsx", "mjs", "cjs"], roots = ["tsconfig.json", "jsconfig.json", "package.json"] }
c = { command = ["clangd"], files = ["c", "h", "cc", "cpp", "cxx", "hpp", "hh"], roots = ["compile_commands.json", "CMakeLists.txt"] }
# zig = { command = ["zls"], files = ["zig"], roots = ["build.zig"] }  # an extension, no dot; one language each

# What holds for every connection, unless its own table says otherwise.
[connections]
stream = true                 # the answer shown as it's written; false, once it's done
activity = true               # and each tool it calls above it, as it calls it
pace = "1s"                   # how often it's shown again, never faster than the platform allows
groups = "mention"            # in a group, which of your messages it answers: mention or never
images = true                 # a message's images reach it; false, it's told each was left out
documents = true              # a message's PDF and text documents, the same
ask_wait = "10m"              # a question for you, a tool call or a page, waits this long: no answer is no
# working = "eyes"            # a reaction while it answers, and steered = "fast_forward" on a message that joined one, where the connection has them

# One connection's own, by its plugin's name. The same keys, over [connections].
[connection.slack]
working = "eyes"              # Slack marks a message with a reaction while it answers
steered = "fast_forward"      # and with this one when the message joined an answer

[connection.telegram]
groups = "never"              # on Telegram it talks in direct messages only

# Who besides you may talk to it: chats on its connections, whoever is in them.
# Left out, only you. The agent's own: never Link's spec's or a team's.
# [access]
# message = ["chat:slack:C0TEAM"]  # anyone in Slack connection C0TEAM, as groups lets you

# Where it would keep each of its collections: a place, its preference.
# The machine has the last word: a place it doesn't support gets its default.
[storage]
conversations = "file"        # its conversations: file, sqlite, bucket, or a place its machine names
# home = "file"               # Home/, the person's: documents and work
# instructions = "file"       # its instructions, which you edit
# skills = "file"             # its skills, which you edit
# memory = "file"             # memory's index
# schedule = "file"           # its scheduled jobs

# How it runs on its machine, where one bridge carries every agent there.
[run]
idle_after = "5m"             # this long with nothing to do, it holds nothing; or "never"

Each feature is on where its keys above turn it on, and off where they're left out, "none", "never" or false; Link's profiles turn some off at once: Features and profiles. Each key is on Agent Spec Keys.

Opinions, not decisions

A decision is in the code: what anyone with the facts would choose, such as Telegram's 4,096 characters a message, or how often its documentation says a bot may write. A model's own figures are facts too, so they are data. Its window, longest answer and price are in Link's file and yours (lnk model show).

An opinion

An opinion is a key here: a trade reasonable people make differently, such as how much of the window to pay for. Link's own opinions are a spec of their own, link_plugin's agent_spec/link.toml, printed whole by lnk agent show --opinions, so someone with the opposite ones changes only the file.

Specs meet as sets

An agent runs three specs as one: Link's, then each of its teams', then its own. Each is a set of keys, and any of them may be partial. The agent runs their union, with a key from a later set winning, key by key. A table of yours replaces only the keys it has.

What lnk agent show --opinions prints

The union, each key marked yours, a team's or Link's, and [storage] as this machine keeps each collection, a place it overrode marked this machine's, naming the one the agent prefers (in --json's from: {"from": "machine", "name": "<preferred>"}).

A general key beats the specific ones before it

Your [model] default replaces Link's [model.anthropic] default too, your [connections] working Link's [connection.slack] working, and [tools] answer a [tool.<name>] answer set before. A specific key in the same set as the general one still wins for its provider, connection or tool. An empty reaction (working = "") is none.

The union must be whole

Every key Link's spec has must be in it, but for those with a fallback ([window] max, the model names and the reactions) and each tool's own ([tool.<name>]). With extends = "none", Link's set is empty, so yours must have them all. A key no set holds refuses the start and names it. A feature's switch is never needed: left out, the feature is off.

A tool's keys

A tool's keys are its tool's to need: a tool the agent runs says which it lacks when it starts (its spec sets no [tool.scheduler] per_day), so a spec that extends none sets only the tables of the tools it runs.

Teams agree, or you settle it

Two teams setting one key differently refuse the start, unless the agent's own spec sets it.

What isn't an opinion isn't yours to claim

name, owner and teams are what Link knows of the agent here: a spec naming another agent, another owner, or a team whose file doesn't list it is refused, and the owner is always Link's, never a file's.

Where is the machine's to grant

[storage] is a preference: each collection is kept where the union says if the machine supports it, else at the machine's default, and lnk agent show --opinions marks each key the machine overrode (Specs).

Counts are yours

rounds, calls and the scheduler's caps take any number from 1 to 65,535. Past what Link is tested with (64 rounds, 128 calls, 100,000 a day, 64 subagents or hops) the start is allowed, and lnk agent start says so.

Storage

Everything an agent keeps is in six collections, each read and written through one storage bridge, whatever keeps it. On file, home is Home/ and each other Agent/<collection>/.

instructions and skills

Its instructions and its skills, which you edit. [instructions]'s files are in the agent's collections, their first part naming which: instructions/base.md is in instructions, and the agent's notes, memory/notes.md, in memory.

home, memory, conversations and schedule

home is the person's: documents, work, and the documents memory searches. The others are memory's notes and index, its conversations, and its scheduled jobs.

How a place keeps one

A place keeps a collection however its driver does, the same to the agent and its tools: the bridge reads, writes, lists and removes, adds to a file at its end, and leases a turn, so two copies of an agent never both write one. A harness from outside Link keeps its own conversations however it does; the rest are Link's.

file

Each collection a folder of the agent's on this machine (Configuration); a conversation is a file only added to. It keeps every collection.

sqlite

Each collection rows of one SQLite file in the agent's folder, lnk-sqlite. It keeps all but home.

bucket

Each collection objects in the agent's bucket, the one lnk bucket connected for it, encrypted, with a cache of memory's index on this machine. It keeps every collection.

A place's driver

A place whose driver isn't installed installs as a plugin a feature needs does. A place's driver refuses what it doesn't serve, and the machine's [storage] never lists a place for a collection its driver refuses.

Values

How a spec writes a size, a duration and how hard a model thinks, and what it refuses.

Sizes and durations

A size is a number and B, KB, MB or GB, powers of 1024. A duration is a number and ms, s, m, h or d.

Thinking

On Anthropic's newer models, the effort the model thinks at, and off the least it can, as some always think. On a model that takes a budget, thinking_budget tokens, under the answer's. On OpenAI's reasoning models, the reasoning effort. A model Link doesn't know isn't asked to think.

A tool's own keys

tool.memory's keys are memory's only, tool.scheduler's the scheduler's, fetch web's, and output run's. Another tool's table with one is refused. Link Harness sends each tool its own table with every call, so one tool shared by many agents follows each one's.

What's refused

An unknown key or table, an empty table, or a value out of range is refused, naming the line.

Where it's kept

Yours is the agent's, a team's is the team's, and neither grants what the agent reaches.

Yours

lnk agent start --spec <file> reads a standalone behavior spec strictly, checks its resolved values, and keeps them under [behavior] in ~/Link/Agents/<agent>/Config/agent.toml, beside its setup. Generated configuration contains concrete defaults without an extends pointer. Examples on this page are standalone specs. When editing saved configuration, prefix their sections with behavior, such as [behavior.tool.file]. Every start reads it again and a move carries it. A standalone spec setting [harness] selects that harness in setup.

Link rewrites only [access], through lnk agent share and unshare, the rest of the file and its comments kept: what a machine overrides is shown, not written back.

Never a grant of what it reaches

What the agent may reach is its sandbox file beside it, sandbox.toml (lnk agent start --sandbox, lnk agent allow): a spec repeats nothing of it (Choose what your agent can reach). A spec's [access] says only who may ask it (Who may talk to it), and whoever may can make it do all its sandbox file allows.

A team's

~/.lnk/teams/<team>.toml, the opinions its members share beneath their own (Team File).

More about it

Why it works this way: decisions.md. Every spec Link reads, side by side: Specs.

Checked reference

The field names below come from link_agent_spec::Spec. Optional values remain unset until the feature resolves its environment or inherited settings.

root: access, command_response, commands, connection, connections, env, extends, goals, harness, instructions, model, name, owner, run, storage, teams, tool, tools, turns, version, vm, window
access: member, message
commands: ask, compact, create, export, fork, goal, help, model, models, queue, rename, resume, skills, status, steer, stop, tasks, verbose, write
connection.*: activity, ask_wait, documents, groups, images, pace, steered, stream, voice, working
connections: activity, ask_wait, documents, groups, images, pace, steered, stream, voice, working
goals: tokens, turns
harness: name
instructions: base, environment, max, notes, now, owner, persona, repository, skills, standing
instructions.max: base, environment, model, notes, now, owner, persona, repository, skills, standing, tools
model: aliases, allow, answer, anthropic, default, fallback, fallback_model, local, ollama, openai, openrouter, thinking, thinking_budget, transcribe
model.anthropic: aliases, allow, answer, anthropic, default, fallback, fallback_model, local, ollama, openai, openrouter, thinking, thinking_budget, transcribe
model.local: aliases, allow, answer, anthropic, default, fallback, fallback_model, local, ollama, openai, openrouter, thinking, thinking_budget, transcribe
model.ollama: aliases, allow, answer, anthropic, default, fallback, fallback_model, local, ollama, openai, openrouter, thinking, thinking_budget, transcribe
model.openai: aliases, allow, answer, anthropic, default, fallback, fallback_model, local, ollama, openai, openrouter, thinking, thinking_budget, transcribe
model.openrouter: aliases, allow, answer, anthropic, default, fallback, fallback_model, local, ollama, openai, openrouter, thinking, thinking_budget, transcribe
run: files, idle_after, let_sleep
storage: conversations, home, instructions, memory, schedule, skills
tool.*: answer, ask, background, background_keep, background_output, background_per_day, background_timeout, diffs, driver, enabled, fetch, find, firing, hops, keep, late, max_age, model, notes, output, per_day, servers, strangers, subagents, timeout, tokens, zone
tool.*.servers.*: command, files, roots
tools: answer, ask, background, background_keep, background_output, background_per_day, background_timeout, diffs, driver, enabled, fetch, find, firing, hops, keep, late, max_age, model, notes, output, per_day, servers, strangers, subagents, timeout, tokens, zone
tools.servers.*: command, files, roots
turns: busy, calls, lapse, renew, rounds
window: answer, compact_at, margin, max, save_notes, unknown

Checked defaults

These values come from the shipped defaults, including nested settings. Omitted optional values have no stored default at this layer.

model = {"thinking":"low","thinking_budget":2048,"fallback":true,"anthropic":{"default":"claude-haiku-5-5"},"openai":{"default":"gpt-6-luna","transcribe":"gpt-4o-mini-transcribe"},"openrouter":{"default":"anthropic/claude-haiku-5.5"}}
window = {"max":128000,"unknown":32768,"margin":0.125,"answer":0.25,"compact_at":0.75,"save_notes":true}
turns = {"rounds":8,"calls":16,"busy":"steer","renew":"10s","lapse":"60s"}
instructions = {"base":"link","environment":"link","skills":"link","persona":"instructions/persona.md","owner":"instructions/owner.md","standing":"instructions/instruction.md","now":"link","notes":"memory/notes.md","repository":"link","max":{"base":"16 KB","model":"8 KB","tools":"8 KB","environment":"4 KB","repository":"16 KB","skills":"8 KB","persona":"8 KB","owner":"8 KB","standing":"32 KB","now":"512 B","notes":"4 KB"}}
commands = {"help":true,"status":true,"create":true,"stop":true,"model":true,"models":true,"tasks":true,"write":true,"goal":true,"resume":true,"fork":true,"rename":true,"export":true,"queue":true,"steer":true,"compact":true,"ask":true,"skills":true,"verbose":true}
command_response = ""
goals = {"turns":10,"tokens":1000000}
tools = {"answer":"32 KB","timeout":"5m"}
tool = {"code":{"enabled":false,"servers":{"c":{"command":["clangd"],"files":["c","h","cc","cpp","cxx","hpp","hh"],"roots":["compile_commands.json","CMakeLists.txt"]},"go":{"command":["gopls"],"files":["go"],"roots":["go.mod"]},"python":{"command":["pyright-langserver","--stdio"],"files":["py","pyi"],"roots":["pyproject.toml","setup.py","pyrightconfig.json"]},"rust":{"command":["rust-analyzer"],"files":["rs"],"roots":["Cargo.toml"]},"typescript":{"command":["typescript-language-server","--stdio"],"files":["ts","tsx","js","jsx","mjs","cjs"],"roots":["tsconfig.json","jsconfig.json","package.json"]}}},"file":{"enabled":false,"find":true,"diffs":true},"memory":{"keep":"10m","max_age":"1h","tokens":6000,"model":"qwen3-embedding","notes":"4 KB"},"run":{"timeout":"2m","enabled":false,"output":"16 KB","background":4,"background_timeout":"1h","background_output":"16 MB","background_keep":"1d","background_per_day":100},"scheduler":{"per_day":100,"subagents":5,"hops":8,"late":"run","ask":false,"zone":"local","firing":"30m","enabled":false,"driver":"service"},"web":{"fetch":"24 KB"}}
connections = {"stream":true,"activity":true,"pace":"1s","groups":"mention","images":true,"documents":true,"voice":true,"ask_wait":"10m"}
connection = {"slack":{"working":"eyes"}}
storage = {"conversations":"log"}
run = {"idle_after":"5m"}