Skip to content

Commit 00acef9

Browse files
committed
v0.9.5: status-line ribbon + Windows install-service fix
Status ribbon - New `burnwall statusline`: renders the Burnwall ribbon for Claude Code's customizable status line from its per-turn stdin JSON, enriched with cross-tool spend and security-block counts from the proxy DB. One-line settings.json wiring; fail-open on bad input. - Canonical ribbon renderer (src/ribbon.rs) with an honest context gauge: exact when the tool reports it, ~marked when estimated, — when untrusted, omitted when the tool shows its own. Reused by upcoming surfaces. - Proxy touches <data dir>/watch.signal after each recorded turn (off the response path) — groundwork for event-driven refresh. Fix - Windows install-service no longer needs admin: default to a per-user HKCU\...\Run entry launching `burnwall start --daemon` at logon; `--task` opts into the elevated Scheduled-Task variant (crash-restart). uninstall-service removes whichever was installed.
1 parent d51ee88 commit 00acef9

14 files changed

Lines changed: 787 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22

33
All notable changes to Burnwall.
44

5+
## [0.9.5] — 2026-06-07
6+
7+
### Added
8+
9+
- **`burnwall statusline`** — renders the Burnwall ribbon for Claude Code's
10+
customizable status line. Reads Claude Code's per-turn JSON on stdin and prints
11+
one line: `🔥 sonnet-4.6 · ↑13k ↓615 · $0.05 msg $0.16 sess · $2.40 today · ctx
12+
[▓▓░░░░░░] 22%`. Per-message cost is derived from the cumulative session total;
13+
today's spend and security-block count are enriched from the proxy database, so
14+
the line reflects spend **across all your tools**, not just the current one.
15+
Wire it up with one line in `~/.claude/settings.json`:
16+
`{ "statusLine": { "type": "command", "command": "burnwall statusline" } }`.
17+
Fail-open: malformed input or an unreadable database still yields a best-effort
18+
line rather than breaking the editor.
19+
- **Context gauge is honest by construction** — the ribbon shows a context-window
20+
percentage only when it's *exact* (reported by the tool, e.g. Claude Code).
21+
Where a value is estimated it's flagged with `~`; where the window can't be
22+
trusted it renders ``; where the tool already shows its own gauge it's omitted
23+
rather than duplicated.
24+
- **Activity marker** — the proxy touches `<data dir>/watch.signal` after each
25+
recorded turn (off the response path, so no added latency), laying the
26+
groundwork for event-driven refresh of upcoming status surfaces.
27+
28+
### Fixed
29+
30+
- **`burnwall install-service` on Windows no longer needs admin.** It previously
31+
created a Scheduled Task at the Task Scheduler library root, which requires
32+
elevation and failed with "Access is denied" for a normal shell. The default is
33+
now a per-user `HKCU\…\Run` registry entry that launches `burnwall start
34+
--daemon` at logon — no UAC. `--task` opts back into the Scheduled-Task variant
35+
(which adds crash-restart) for users who run an elevated terminal.
36+
`uninstall-service` removes whichever was installed.
37+
538
## [0.9.4] — 2026-06-07
639

740
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "burnwall"
3-
version = "0.9.4"
3+
version = "0.9.5"
44
edition = "2024"
55
rust-version = "1.87"
66
description = "Local proxy for AI coding tools (Claude Code, Codex CLI, Aider): cache-aware cost tracking, path/command security checks, daily budget enforcement. Zero telemetry."

editor/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "burnwall",
33
"displayName": "Burnwall",
44
"description": "Cost + security for your AI coding agents, at a glance — reads your local Burnwall CLI.",
5-
"version": "0.9.4",
5+
"version": "0.9.5",
66
"publisher": "intbot",
77
"license": "FSL-1.1-MIT",
88
"repository": { "type": "git", "url": "https://github.com/intbot/burnwall" },

packaging/mcp/server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"url": "https://github.com/intbot/burnwall",
77
"source": "github"
88
},
9-
"version": "0.9.4",
9+
"version": "0.9.5",
1010
"packages": [
1111
{
1212
"registryType": "oci",

src/cli/init.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,11 @@ pub fn run_cmd(args: InitArgs) -> anyhow::Result<()> {
289289
let exe = std::env::current_exe().context("locating burnwall executable")?;
290290
// Call platform install path directly — same code the
291291
// install-service command runs.
292-
super::service::install_cmd(super::service::InstallServiceArgs { no_start: false })
293-
.with_context(|| format!("installing service for {}", exe.display()))?;
292+
super::service::install_cmd(super::service::InstallServiceArgs {
293+
no_start: false,
294+
task: false,
295+
})
296+
.with_context(|| format!("installing service for {}", exe.display()))?;
294297
} else {
295298
writeln!(out, " {action_label}: register login-time service")?;
296299
}

src/cli/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub mod self_rollback;
3333
pub mod service;
3434
pub mod start;
3535
pub mod status;
36+
pub mod statusline;
3637
pub mod stop;
3738
#[cfg(feature = "waste")]
3839
pub mod waste;
@@ -103,6 +104,8 @@ pub enum Command {
103104
SelfRollback(self_rollback::SelfRollbackArgs),
104105
/// Inspect and manage the pricing rate card (local + signed remote cards).
105106
Pricing(pricing::PricingArgs),
107+
/// Render the Burnwall ribbon for Claude Code's status line (reads stdin JSON).
108+
Statusline(statusline::StatuslineArgs),
106109
}
107110

108111
impl Cli {
@@ -141,6 +144,7 @@ impl Cli {
141144
Command::UninstallService(args) => service::uninstall_cmd(args),
142145
Command::SelfRollback(args) => self_rollback::run_cmd(args),
143146
Command::Pricing(args) => pricing::run_cmd(args),
147+
Command::Statusline(args) => statusline::run_cmd(args),
144148
}
145149
}
146150
}

src/cli/service.rs

Lines changed: 118 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@
1111
//! `~/.config/systemd/user/burnwall.service`. `Restart=on-failure` with
1212
//! `StartLimitBurst=5` + `StartLimitIntervalSec=60` is the same crash-loop
1313
//! circuit breaker shape.
14-
//! - **Windows** — a per-user Scheduled Task triggered at logon, registered
15-
//! via `schtasks.exe`. Task Scheduler restarts on failure (5 attempts at
16-
//! 1-min intervals) — same shape, different incantation.
14+
//! - **Windows** — by default, a per-user `HKCU\…\CurrentVersion\Run` registry
15+
//! entry that launches `burnwall start --daemon` at logon. This needs **no
16+
//! admin / UAC** (the earlier Scheduled-Task default failed with "Access is
17+
//! denied" because creating a task at the library root requires elevation).
18+
//! `--task` opts into the Scheduled-Task variant instead — it adds
19+
//! crash-restart (5 attempts at 1-min intervals) but must be run from an
20+
//! elevated terminal.
1721
//!
18-
//! ## No admin required
22+
//! ## No admin required (by default)
1923
//!
20-
//! All three install user-scoped services that need no admin / sudo / UAC.
21-
//! Per-user is the right scope because the proxy serves one user's traffic
22-
//! through env vars in their shell.
24+
//! Every default path installs a user-scoped service that needs no admin /
25+
//! sudo / UAC. Per-user is the right scope because the proxy serves one user's
26+
//! traffic through env vars in their shell. (Windows `--task` is the one opt-in
27+
//! that needs elevation, in exchange for crash-restart.)
2328
2429
use std::path::PathBuf;
2530

@@ -36,14 +41,19 @@ pub struct InstallServiceArgs {
3641
/// Skip the start step (just register the service, don't launch it).
3742
#[arg(long)]
3843
pub no_start: bool,
44+
/// Windows only: register a Scheduled Task (adds crash-restart) instead of
45+
/// the default per-user Run-key entry. Must be run from an elevated
46+
/// terminal. Ignored on macOS/Linux.
47+
#[arg(long)]
48+
pub task: bool,
3949
}
4050

4151
#[derive(Args, Debug)]
4252
pub struct UninstallServiceArgs {}
4353

4454
pub fn install_cmd(args: InstallServiceArgs) -> Result<()> {
4555
let exe = std::env::current_exe().context("locating burnwall executable")?;
46-
install(&exe, !args.no_start)
56+
install(&exe, !args.no_start, args.task)
4757
}
4858

4959
pub fn uninstall_cmd(_args: UninstallServiceArgs) -> Result<()> {
@@ -93,7 +103,7 @@ fn plist_contents(exe: &std::path::Path) -> String {
93103
}
94104

95105
#[cfg(target_os = "macos")]
96-
fn install(exe: &std::path::Path, start: bool) -> Result<()> {
106+
fn install(exe: &std::path::Path, start: bool, _task: bool) -> Result<()> {
97107
let path = plist_path()?;
98108
if let Some(parent) = path.parent() {
99109
std::fs::create_dir_all(parent)
@@ -170,7 +180,7 @@ WantedBy=default.target
170180
}
171181

172182
#[cfg(target_os = "linux")]
173-
fn install(exe: &std::path::Path, start: bool) -> Result<()> {
183+
fn install(exe: &std::path::Path, start: bool, _task: bool) -> Result<()> {
174184
let path = unit_path()?;
175185
if let Some(parent) = path.parent() {
176186
std::fs::create_dir_all(parent)
@@ -292,8 +302,56 @@ fn task_xml(exe: &std::path::Path) -> String {
292302
)
293303
}
294304

305+
/// HKCU autostart key — writable by a standard user, no admin needed.
306+
#[cfg(target_os = "windows")]
307+
const RUN_KEY: &str = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
308+
309+
#[cfg(target_os = "windows")]
310+
fn install(exe: &std::path::Path, start: bool, use_task: bool) -> Result<()> {
311+
if use_task {
312+
install_scheduled_task(exe, start)
313+
} else {
314+
install_run_key(exe, start)
315+
}
316+
}
317+
318+
/// Default Windows autostart: a per-user `HKCU\…\Run` value that launches
319+
/// `burnwall start --daemon` at logon. No admin required. Written via `reg.exe`
320+
/// so we don't pull in a registry crate.
321+
#[cfg(target_os = "windows")]
322+
fn install_run_key(exe: &std::path::Path, start: bool) -> Result<()> {
323+
// The exe path is quoted so a profile path with spaces still parses at logon.
324+
let command = format!("\"{}\" start --daemon", exe.display());
325+
let status = std::process::Command::new("reg")
326+
.args([
327+
"add", RUN_KEY, "/v", TASK_NAME, "/t", "REG_SZ", "/d", &command, "/f",
328+
])
329+
.stdout(std::process::Stdio::null())
330+
.stderr(std::process::Stdio::null())
331+
.status()
332+
.context("running reg add")?;
333+
if !status.success() {
334+
anyhow::bail!(
335+
"reg add failed (status {status}). You can still run `burnwall start --daemon` \
336+
manually, or try `burnwall install-service --task` from an elevated terminal."
337+
);
338+
}
339+
println!("🛡 Registered login auto-start (HKCU Run): {TASK_NAME}");
340+
println!(" Launches `burnwall start --daemon` at logon — no admin required.");
341+
if start {
342+
start_daemon_now(exe);
343+
} else {
344+
println!(" (not started — will start at next logon)");
345+
}
346+
println!(" Tip: `--task` installs a Scheduled Task with crash-restart (needs an elevated terminal).");
347+
Ok(())
348+
}
349+
350+
/// Opt-in Windows autostart: a per-user Scheduled Task at logon. Adds
351+
/// crash-restart, but creating the task at the library root requires
352+
/// elevation — so this must be run from an Administrator terminal.
295353
#[cfg(target_os = "windows")]
296-
fn install(exe: &std::path::Path, start: bool) -> Result<()> {
354+
fn install_scheduled_task(exe: &std::path::Path, start: bool) -> Result<()> {
297355
let xml_path = task_xml_path()?;
298356
if let Some(parent) = xml_path.parent() {
299357
std::fs::create_dir_all(parent)
@@ -320,15 +378,23 @@ fn install(exe: &std::path::Path, start: bool) -> Result<()> {
320378
"/XML",
321379
xml_path.to_str().unwrap_or(""),
322380
])
381+
.stdout(std::process::Stdio::null())
382+
.stderr(std::process::Stdio::null())
323383
.status()
324384
.context("running schtasks /Create")?;
325385
if !status.success() {
326-
anyhow::bail!("schtasks /Create failed (status {})", status);
386+
anyhow::bail!(
387+
"schtasks /Create failed (status {status}) — this usually means it wasn't run \
388+
elevated. Run from an Administrator terminal, or drop `--task` to use the \
389+
no-admin Run-key install instead."
390+
);
327391
}
328392
println!("🛡 Installed Scheduled Task: \\{TASK_NAME}");
329393
if start {
330394
let s = std::process::Command::new("schtasks.exe")
331395
.args(["/Run", "/TN", TASK_NAME])
396+
.stdout(std::process::Stdio::null())
397+
.stderr(std::process::Stdio::null())
332398
.status()
333399
.context("running schtasks /Run")?;
334400
if !s.success() {
@@ -344,17 +410,48 @@ fn install(exe: &std::path::Path, start: bool) -> Result<()> {
344410
}
345411

346412
#[cfg(target_os = "windows")]
347-
fn uninstall() -> Result<()> {
348-
let status = std::process::Command::new("schtasks.exe")
349-
.args(["/Delete", "/F", "/TN", TASK_NAME])
413+
fn start_daemon_now(exe: &std::path::Path) {
414+
match std::process::Command::new(exe)
415+
.args(["start", "--daemon"])
350416
.status()
351-
.context("running schtasks /Delete")?;
352-
if status.success() {
417+
{
418+
Ok(s) if s.success() => println!(" Started."),
419+
_ => println!(" (could not start now — will start at next logon)"),
420+
}
421+
}
422+
423+
#[cfg(target_os = "windows")]
424+
fn uninstall() -> Result<()> {
425+
let mut removed = false;
426+
// Default install: the HKCU Run-key value. Probes are best-effort — silence
427+
// child stdout/stderr so a missing entry doesn't print a scary "ERROR".
428+
if matches!(
429+
std::process::Command::new("reg")
430+
.args(["delete", RUN_KEY, "/v", TASK_NAME, "/f"])
431+
.stdout(std::process::Stdio::null())
432+
.stderr(std::process::Stdio::null())
433+
.status(),
434+
Ok(s) if s.success()
435+
) {
436+
println!("🛡 Removed login auto-start (HKCU Run): {TASK_NAME}");
437+
removed = true;
438+
}
439+
// Opt-in install: the Scheduled Task.
440+
if matches!(
441+
std::process::Command::new("schtasks.exe")
442+
.args(["/Delete", "/F", "/TN", TASK_NAME])
443+
.stdout(std::process::Stdio::null())
444+
.stderr(std::process::Stdio::null())
445+
.status(),
446+
Ok(s) if s.success()
447+
) {
353448
println!("🛡 Removed Scheduled Task: \\{TASK_NAME}");
354-
} else {
355-
println!("🛡 No Scheduled Task to remove (or removal failed).");
449+
removed = true;
450+
}
451+
if !removed {
452+
println!("🛡 No Burnwall login service found to remove.");
356453
}
357-
// Best-effort cleanup of the staged XML.
454+
// Best-effort cleanup of any staged task XML.
358455
if let Ok(xml_path) = task_xml_path() {
359456
let _ = std::fs::remove_file(&xml_path);
360457
}
@@ -364,7 +461,7 @@ fn uninstall() -> Result<()> {
364461
// ─────────────────────────── unsupported ───────────────────────────
365462

366463
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
367-
fn install(_exe: &std::path::Path, _start: bool) -> Result<()> {
464+
fn install(_exe: &std::path::Path, _start: bool, _task: bool) -> Result<()> {
368465
anyhow::bail!("install-service is not supported on this OS");
369466
}
370467

0 commit comments

Comments
 (0)