@telosfoundry/cli
v0.2.0
Published
Command-line client for the Telos REST API
Downloads
46
Readme
Telos CLI
Install the Telos command-line client globally:
npm install --global @telosfoundry/cliCreate an API key under Settings → API Keys, then authenticate and run a command:
export TELOS_API_KEY=telos_live_…
telos customer listTELOS_API_URL defaults to https://www.telos-app.com and can be overridden
for another Telos instance.
Attributing coding sessions to tasks
The collector reads local Codex and Claude Code session logs, keeps a marker of what you were working on, and uploads counters, timestamps, models and hashed identifiers under your own account. It never runs itself: no hook, no daemon, nothing on a timer.
telos track TEL-182 # start a marker (also linear:ENG-14, jira:PLT-3, unassigned)
telos switch TEL-183 # close the open marker, open another
telos stop # close the open marker without opening another
telos status # open marker, sessions since your last submit, state file path
telos submit # review the exact payload, then upload
telos collector off # stop collecting; submit refuses until you turn it back ontelos submit builds and prints a complete locally hashed payload before any
network request. The first submit, and any submit after the schema or collector
version changes, requires consent before the CLI contacts Telos; --yes cannot
bypass it. The CLI then prints the final server-keyed payload, prints it again
after every allocation edit, and uploads only the exact object you confirm.
--yes skips confirmation only for routine submits after that.
--skip-unsupported leaves behind
session logs from a client version this release cannot parse, which otherwise
stop the whole submit.
No prompt, response, thinking text, tool payload, file path, environment value
or credential can reach the payload. Session and response identifiers must have
their provider's documented shape. They, your working directory and your branch
are SHA-256 hashed locally before the server applies the workspace-keyed HMAC,
so no raw identifier reaches Telos. Task and external-reference targets are
bounded tokens and credential-like values are refused. Deleting a local log
deletes nothing already uploaded — telos observed-evidence delete-self does
that. Your own week is visible in the app under Time → My Time → Observed
evidence, and nobody else can read it without a time-boxed grant an admin
issues with a reason.
State lives in ~/.telos/collector/state.json, or under $XDG_STATE_HOME, or
wherever TELOS_COLLECTOR_DIR points; telos status prints the path in use.
The strict versioned file is atomically replaced with mode 0600. It keeps a
byte-length and mtime watermark per session and always re-reads sessions that
were open at the previous submit.
Mirror the Direction store
telos direction pull writes every published vision, strategy, product
spec and context document into your repository as markdown with frontmatter, so
a coding agent reads your direction from the filesystem instead of asking the
API for it on every task.
The key
Create one in Telos under Settings → API Keys → New key with the grants
direction:read, vision:read and strategy:read. A key without
vision:read or strategy:read mirrors the product specs and context
documents alone; the rest of the store is simply not there.
export TELOS_API_KEY=telos_live_…TELOS_API_URL defaults to https://www.telos-app.com and points the mirror
at another Telos instance.
Install the session-start hook
telos direction initinit writes the hook that keeps the mirror fresh into five places, creating
each file if it is missing and leaving every other hook in it alone:
| Tool | File |
| --- | --- |
| Claude Code | .claude/settings.json |
| Codex | .codex/hooks.json (trust it once: open Codex in the repo and run /hooks) |
| Gemini | .gemini/settings.json |
| Copilot | .github/hooks/telos-direction.json, plus ~/.copilot/hooks/telos-direction.json on this machine |
| Cursor | .cursor/hooks.json |
The hook itself is npx -y @telosfoundry/cli direction sync, so the repository
needs nothing installed. init also appends a short block to your repository's
AGENTS.md telling an agent to read the mirror before it plans, and adds
.telos/direction/ to .gitignore. Running it twice changes nothing. A file
it cannot parse is skipped with the command to add by hand.
Claude Code runs the hook from the next session. Codex skips a repository's
hooks until the project is trusted and the hook itself has been reviewed once
in /hooks; it records trust against the hook's hash, so init never edits
Codex's own config for you. Codex also ignores project hooks inside a git
worktree. Verified on 2026-09-14 against Claude Code 2.1 and Codex 0.154: the
Claude hook rebuilt a deleted mirror on session start; the Codex hook ran only
after that one-time trust.
Copilot CLI 0.59 ran the hook from ~/.copilot/hooks/ and did not load the
same file from the repository's .github/hooks/ in a headless session
(github/copilot-cli issue 1730). So init also writes
~/.copilot/hooks/telos-direction.json on the machine it runs on, guarded to
run only in a repository that carries .github/hooks/telos-direction.json;
other repositories are untouched. Cursor's agent CLI ran the hook from
.cursor/hooks.json. The Gemini file follows its documented format and has
not been run here.
The four commands
telos direction sync # check, pull when the store moved, never fail
telos direction status # 0 fresh, 1 stale, 2 no manifest, 3 other workspace
telos direction status --verify # also diff every mirrored file against the store
telos direction pull # write .telos/direction/ and its manifest
telos direction pull --dir docs/direction
telos direction pull --reset # wipe the directory and pull this workspace
telos direction pull --force-dir # take over a directory that is not yet a mirrorsync is what the hook runs. It asks the store for one hash, pulls only when
that hash moved, and exits 0 whatever goes wrong: an unreachable server, a
refused key or a missing TELOS_API_KEY each print one line on stderr and let
the session start. Only a mirror belonging to another workspace exits non-zero
(3), because pulling over it would mix two organizations into one tree.
status is the same check without the pull. It reads one hash and compares it
against the manifest, so it touches no file in the mirror and costs one
request. --verify adds the full local walk: every mirrored file is hashed,
strays and hand edits are reported, and the exit code is 1 when anything
differs.
pull does the work. Files land at .telos/direction/<kind>/<ref>.md, for
example .telos/direction/vision/vis-1.md. The path carries the ref alone, so
renaming a document in Telos never moves its file. manifest.json next to them
records the workspace, the pull time, the store's hash and each document's
version, content hash and file hash; pull refetches only the documents whose
version, hash, title or publish time moved, or whose file was edited or deleted
locally.
The generated index
Every pull writes .telos/direction/AGENTS.md: where the documents came from
and when, the vision and strategy to read first, then the product specs and
context documents, each with its title, version and path, and the rule for
telling a stale mirror from a fresh one. It is the one file to point an agent
at. It is recorded in the manifest, so the fully-managed rule below keeps it
rather than deleting it as a stray.
Without the CLI
The same data is on the Telos MCP server: get_direction_head returns the one
workspace hash, list_direction_documents returns the rows and get_direction
returns one document's body. That path works, and it is not the recommended
one: every read spends model tokens on every session, and nothing makes an
agent take it. The mirror is a file on disk that a hook keeps current, which is
why it is enforced and the MCP path is not.
The directory is fully managed. Everything under it that the manifest does
not account for is deleted on the next pull, including your own notes. Point
--dir somewhere else before you put anything of your own next to the mirror,
and git-ignore the directory: it is a cache a command reproduces, not source.
Because it deletes, the mirror refuses a --dir that is plainly not its own,
and no flag lifts those refusals: the filesystem root, your home directory, the
working directory or any parent of it, a symlink, a directory holding a .git
entry, and any directory inside a repository other than the one your working
directory is in. Inside your own repository the mirror is welcome anywhere
below the root that is not a parent of your working directory, which is where
the default .telos/direction sits. Those comparisons ask the filesystem
which directory a path is rather than reading its name, so a differently
spelled or short-named parent of your working directory is refused as the same
directory, and two checkouts that differ only in case on a case-sensitive
volume stay two. --force-dir lifts one refusal and only one: a directory that
already holds files and no manifest.json this CLI wrote.
The mirror does not follow symlinks. Both commands plan before they act: they
name every path the run may touch (each document and its temporary file, kept
documents included, every deletion, manifest.json and its temporary file) and
walk each one a directory at a time from the mirror root down, requiring every
planned path to be absent or a regular file. A symlink or an obstacle anywhere
on any of those paths stops the command before it fetches anything, deletes
anything or wipes anything, status and --reset included. pull then
downloads every changed document, walks the whole plan again, and only then
writes, and each write and each deletion walks its own path once more
immediately before the syscall that begins it. The rename that finishes a
write is not re-walked separately, which the threat model below covers.
What this defends against. The mirror defends against a pre-existing symlink, misconfiguration or a malicious server; it does not defend against a concurrent local process that swaps symlinks between two of its own syscalls. Node's fs exposes no openat/O_NOFOLLOW-relative primitives, so a race-free implementation is not available in this runtime, and on the single-user developer machine the CLI is built for, a process with that power already owns the checkout; the mirror is not a boundary between users. Every guard in the mirror is therefore "checked immediately before use", not "atomic with use".
A symlink somewhere else under the mirror is not the mirror's business: it is
reported on stderr and left alone, and it does not make status --verify
stale. That is the one case where status prints a complaint and still exits
0, so the hook does not loop on a link the mirror declines to delete.
--reset is the exception: it removes the whole directory, so such a link goes
with it. The link is unlinked, whatever it pointed at is left alone, and the
command says which links it removed.
Two things the mirror cannot see: a cloned volume that reports the same serial and file ids as another one looks like the same volume to every program on the machine, and a bind mount of your own files into the mirror on the same device looks like an ordinary directory. Do not mount anything into the mirror. A mount that reports a different device is refused rather than walked into.
The preflight is all or nothing, the apply is not: if the filesystem fights
back midway through writing (a full disk, a permission change), some documents
may be newer than the manifest. The next pull sees the hash mismatch and
repairs them.
A key from another workspace refuses rather than mixing two organizations into
one tree, and --reset is the way to switch.
License
MIT © Telos Foundry ApS
