Codeep

Approvals & Audit

Two halves of the same idea. The audit record says what an agent did after the fact. Remote approval lets you answer, from anywhere, the question it stopped to ask.

The audit record

Every agent run appends to .codeep/audit/ in the project — one JSON line per event, one file per day. Read it back with /audit:

$ /audit
✓ 24 Aug, 16:26 · Git Only (git) · 1s
inspect the repo
command, refused
⨯ refused: read_file → secrets.env

The refusal on that last line is the reason this exists. A capability boundary that nothing records is a boundary you have to take on faith — the run looks identical whether the agent stayed inside it or was never tested against it.

What it does and does not keep

RecordedNot recorded
File paths, commands, URLsFile contents, ever
Reads, writes, searches, fetchesAnything from a project you did not run in
Tool calls the boundary refusedYour prompts to other agents
Which custom agent ran, and what it was granted

Contents are deliberately absent. Undo already keeps them — that is what /undo and checkpoints are for. This record answers a different question, and a record you might hand to someone else should not carry your source with it.

⚠
Treat it like shell history. A command line can contain a secret somebody typed into it, and this file keeps command lines. It sits under .codeep/, which most projects already ignore — but Codeep never edits your .gitignore, so check that yours does.

On every surface, in one place

The record lives with the project, not with the app that wrote it — same directory, same format, same field names everywhere. A project you work on from the terminal on Monday and the Mac app on Tuesday has one history, and either can read the other's runs.

Reading itTurning it off
CLI/audit/audit off
Mac appOpen the project in the CLI, or read .codeep/audit/ directlySettings → Tools & Privacy → Audit record
VS CodeCodeep: Show Audit RecordUses the CLI's setting

Records already written are kept. Nothing is deleted by turning it off.

Approval on your phone

When a run stops on a dangerous tool it waits — and if you have walked away, it waits for as long as you are gone. Connect Telegram and the same question arrives on your phone with the same three answers. Answer in either place; whichever comes first decides, and the other is taken down rather than left showing buttons that no longer do anything.

ℹ
Both the CLI and the Mac app. This is where Telegram earns its keep: the Mac can carry approvals through your own iCloud, but Linux and Windows have nothing of the sort, and the bot API is polled outbound — no server to host, no inbound port. In the terminal the question appears whenever Agent Confirmation would have asked, which by default is before every dangerous tool.
⚠
Interactive runs only. A headless run — codeep review --fix, the ACP server, CI — has nobody to ask, and blocking one on an answer that cannot come would hang a pipeline. Those decide from agentConfirmation as before.

Setting it up

1. Make a bot. Open Telegram, message @BotFather, send /newbot, and follow the prompts. It replies with a token that looks like 123456789:AAE….

2. Send your bot a message. Any message. A bot cannot start a conversation with you, so this step is what makes the next one possible.

3. Find your chat ID. This is the part that is not obvious. Open this URL in a browser, with your token in place of <TOKEN>:

https://api.telegram.org/bot<TOKEN>/getUpdates

Look for "chat":{"id":…} in the response. That number is your chat ID.

4. Enter both. Where depends on which you are setting up — the token goes to your OS keychain either way, and the chat ID is ordinary settings.

TokenChat ID and the switch
CLI/telegram — the input is masked/settings → Telegram chat ID, then Answer confirmations on Telegram
Mac appSettings → Tools & Privacy → Approval on your phoneSame panel, same section
ℹ
One bot serves both. The same token and chat ID work in the terminal and in the app — there is no need for a second bot, and the message says which one is asking.

What it looks like

The message names the tool, shows the command, and offers three buttons — Run, Skip, Cancel — the same three the terminal and the desktop sheet offer. Answer there instead and the message updates to say where it was decided, rather than leaving buttons that no longer do anything.

There is deliberately no Always allow on the phone. A standing permission is a decision that belongs at the keyboard, where you can see what you are granting it to.

⚠
Only your chat ID can answer. A bot's username is discoverable, so every incoming answer is checked against the ID you entered. Without that check, anyone who found your bot could approve a destructive command on your Mac. Do not share the token — it is the bot.
ℹ
Telegram sees the command. The message has to contain it, or you cannot judge what you are approving. If that is not acceptable for your work, leave this off — the desktop confirmation is unchanged and still authoritative.

If it does not arrive

SymptomUsually
Nothing ever arrivesThe switch is off, or the chat ID is empty
"chat not found"The ID belongs to a chat with a different bot — or you have not messaged this one yet
Worked once, then stoppedToken was regenerated in BotFather — enter the new one
Buttons do nothingThe question was already answered on the Mac
getUpdates returns an empty resultYou have not messaged the bot yet — do step 2

Starting a task from your phone

The section above lets your phone answer a question the agent chose to ask. This lets it ask one. Send the bot an instruction in plain words and it runs, as though you had typed it into the terminal — and the answer comes back to the same chat.

⚠
This is a bigger switch than the one above, and it is separate on purpose. A chat that can only say Run, Skip or Cancel is bounded by what the agent already decided to do. A chat that can send a prompt is a keyboard attached to your machine. Both are gated on the same chat ID, but they are not the same permission, so turning one on does not turn on the other. This one is off until you ask for it.

Turning it on

The token and chat ID are the ones you already entered above; there is no second bot and no second setup. In the CLI, /settings → Start tasks from Telegram, then restart the CLI. In the Mac app, Settings → Telegram → Start tasks from Telegram, which takes effect straight away. Then message the bot.

ℹ
One bot, one machine at a time. Telegram gives a bot a single update cursor, so whichever side reads a message is the only side that sees it. Running the CLI and the Mac app against the same bot means instructions land on whichever got there first. Use one, or make a second bot for the other.

Which conversation it drives

In the Mac app, right-click a conversation in the sidebar and choose Drive this from Telegram. A paper plane marks it, and every instruction from your phone lands there no matter what you are looking at — so you can read one conversation while the phone works in another. Choose it again to release it.

With nothing chosen, instructions go to the selected conversation. That keeps the feature usable before you have picked one, but it also means the target moves when you switch conversations at the Mac. Pick one for anything you plan to leave running.

While a run is in flight

An instruction sent while the agent is busy waits, and the bot says so. One waits at a time: send a second and it replaces the first, because two corrections typed during one long run mean the second, not both in order. The reply tells you when it displaced something rather than dropping it quietly.

An instruction waits behind a run you started at the machine too. The queue does not care where the current work came from — only that something is already using the agent.

What it will not run

SentWhat happens
A message from any other chatIgnored, with no reply at all — answering confirms the bot is live and attached to something worth probing
A photo, sticker or voice noteRefused: text only
Anything starting with /Refused — those are addressed to the bot, not to the agent
More than 2000 charactersRefused: that is a paste, not an instruction
A message older than five minutesRefused, and it says so
⚠
Why five minutes. Telegram holds undelivered messages for 24 hours. Without that rule, an instruction typed at midnight to a machine that was switched off would run the moment the CLI or the app next opened — hours later, in a repository that has moved on, with nobody watching. A prompt is an instruction for now.

The answer comes back

A run started from your phone sends its answer to your phone, because "finished, 33K tokens" answers nothing you asked. A run you started at the machine does not, by default: the answer is already on the screen you are sitting at, and sending it would put file contents or a command line into a chat for nothing. If you start something and walk away, turn on Send terminal answers to Telegram in the CLI's /settings, and the "finished" message carries the answer too (CLI 3.8.0 and later). In the Mac app (3.8.1 and later) it is Send answers to Telegram under Settings → Tools & Privacy → Approval on your phone: a run you started at the Mac that takes a minute or longer sends how it ended and the answer. Long answers are cut, with a line saying they were, and markdown is stripped — an answer should read as words on a phone, not as **words**.

ℹ
Approvals still apply. A task started from the phone is not a task exempt from confirmation. If it reaches a dangerous tool it stops and asks, exactly as it would have done for you at the keyboard — and that question arrives on the same phone.