* feat(daemon): add CC_LOG_MAX_BACKUPS env var support (#1222)
PR #1243 only addressed CC_LOG_MAX_SIZE while leaving the backup
count hard-wired to one (.log.1). That still loses any post-mortem
context older than one rotation, which is the same class of failure
users reported on #1222. This change adds the matching knob so the
post-mortem trail is configurable, with the same flag > env >
default priority used for size.
- daemon: add ParseLogBackups(s) (>=1, no unit suffix, error echoes
input) and DefaultLogMaxBackups = 3.
- daemon: extend RotatingWriter with maxBackups; rotateLocked walks
the chain (delete .N, shift .(N-1) -> .N .. .1 -> .2, rename
active -> .1, reopen) and a public Rotate() hook for tests/SIGHUP.
- daemon: Config/Meta gain LogMaxBackups; Resolve() defaults to 3.
- cmd/cc-connect: resolveLogMaxBackups + preScanLogMaxBackupsFlag +
--log-max-backups flag; startup log now reports max_backups and
its source. The rotating-writer setup happens before flag.Parse so
the pre-scan keeps the flag effective there too.
- daemon/launchd.go, daemon/systemd.go, daemon/windows.go: thread
CC_LOG_MAX_BACKUPS through the service templates so a fresh
install picks it up.
- tests: TestParseLogBackups (19 subtests + error-echo), three new
RotatingWriter tests (chain, disabled, fallback), four resolver
tests + pre-scan tests in cmd/cc-connect. TestIssue1222_BackupRetention
pins the new env-var behaviour as the regression test for the
follow-up to #1222.
* fix(daemon): silence errcheck on logrotate_test.go defer Close
QA review (run 27109765660) flagged defer w.Close() in the 4
backup-related tests added by #1260. Wrap each in
defer func() { _ = w.Close() }() so errcheck is satisfied without
changing test semantics (temp-dir cleanup is best-effort).
Verified locally:
- golangci-lint --new-from-rev origin/main ./daemon/... -> 0 issues
- go test -count=1 -tags no_web ./daemon/ ./cmd/cc-connect/ -> ok
---------
Co-authored-by: cc-connect dev-claudecode <dev-claudecode@cc-connect.local>
Co-authored-by: Claude <noreply@anthropic.com>
PR #1034 (c53f5450) re-introduced daemon/linger_other.go with
`//go:build !linux`, which overlaps every per-platform stub:
- launchd.go (darwin)
- windows.go (windows)
- unsupported.go (other)
This causes `CheckLinger redeclared in this block` and breaks
`make release-all` on every non-Linux GOOS target.
Per-platform stubs already cover all four cases:
- linux → daemon/systemd.go
- darwin → daemon/launchd.go
- windows → daemon/windows.go
- other → daemon/unsupported.go
Delete the redundant `!linux` stub. Same fix as the never-merged
PR #1311; no runtime behavior change.
Verified:
- GOOS=linux go build ./daemon/... → ok
- GOOS=darwin go build ./daemon/... → ok
- GOOS=windows go build ./daemon/... → ok
- GOOS=freebsd go build ./daemon/... → ok
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(daemon): harden service-file writes and capture config.toml ${ENV}
Three independent improvements to the service-file install path so
operators can store API keys / tokens in config.toml using ${ENV}
placeholders and have them work with the installed daemon, without
those values being world-readable on disk.
1. captureConfigEnvPlaceholders: during `daemon install`, scan the
target config.toml for ${VAR_NAME} placeholders. For every match
that is set in the current process environment, copy the value
into the EnvExtra map so the rendered launchd plist / systemd unit
/ Windows task script carries it. Without this, the daemon
process starts with empty strings for placeholder values and
fails to authenticate to any platform.
2. Tighten service-file permissions to 0600. Both the WriteFile mode
and an explicit os.Chmod after write — the Chmod is required for
the reinstall path because WriteFile only applies perm on create,
so a 0644 file left by a previous cc-connect version would keep
its old permissions in place. Applies to launchd plist, systemd
unit, and Windows .ps1 script (the latter as a defense-in-depth
layer; real Windows access control lives in the ACL).
3. EnvExtra hardening in every renderer:
- drop entries whose key fails POSIX-identifier validation
- drop entries whose value is empty
- launchd: XML-escape both keys and values; reserve PATH /
CC_LOG_FILE / CC_LOG_MAX_SIZE so EnvExtra can't override the
template-owned keys
- systemd: backslash- and quote-escape values for the
`Environment="K=V"` form per systemd.exec(5)
Add --no-capture-secrets / CC_DAEMON_NO_CAPTURE_SECRETS=1 opt-out for
operators who'd rather inject secrets via keychain, secret-tool, or
systemd EnvironmentFile= and keep the service file token-free.
runSystemctl becomes a var so per-platform tests can stub it without
needing a real systemctl on the host.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(daemon): add EnvDiscoverer plugin hook for install-time env capture
Generalises captureDaemonEnv() with a small plugin registry:
type EnvDiscoverer func() (map[string]string, error)
daemon.RegisterEnvDiscoverer(d)
daemon.ResetEnvDiscoverers() // tests only
Resolve() invokes every registered discoverer (unless NoCaptureSecrets
is set) and merges the result into EnvExtra after the proxy-key capture
and config.toml ${ENV} placeholder scan. Discoverers run in
registration order; the returned map is filtered for valid POSIX env
names and non-empty values before reaching any renderer.
Discoverer errors are logged at WARN level and never fail install —
matching the rest of the install path's tolerance posture.
Use case: lets a downstream / plugin agent contribute extra env vars
the service file should carry, without daemon needing to know about
that agent. Without the hook, the only ways to extend capture are (a)
pre-populate EnvExtra in the caller — verbose at every install site —
or (b) edit daemon/manager.go to import the new package — which couples
daemon to that package and breaks the no-agent-deps boundary.
The isValidEnvName helper moves out of manager.go into the new
env_extension.go alongside the registry, since the validator is shared
between the renderers and the discoverer-merge code.
Comment tweaks in launchd.go / systemd.go acknowledge that discoverer
output is another source of captured secrets justifying the 0600 perms.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(daemon): add non-linux linger check stub
---------
Co-authored-by: Shuchao Shao <shaoshch@yonyou.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
In v1.3.3-beta.4 the rotating-writer setup at startup called
strconv.ParseInt on CC_LOG_MAX_SIZE, so any value carrying a unit suffix
("10MB", "512K", "1G") silently failed and the env var was ignored. The
default 10MB was used, the user got no warning, and unit-suffix
configurations appeared to take effect but did not.
Introduce daemon.ParseLogSize, which accepts raw bytes plus
case-insensitive K/KB/M/MB/G/GB/T/TB/B suffixes with binary (1024-based)
multipliers, and a resolveLogMaxSize helper that applies the priority
order: --log-max-size flag > CC_LOG_MAX_SIZE env var > built-in default.
A new --log-max-size flag exposes the same parser on the command line.
A startup line ("log: redirecting to ... max_size=... bytes (source:
flag|env|default)") makes the active value auditable without grepping
systemd/launchd definitions. Invalid values are warned to stderr and
skipped, never silently downgraded.
New unit tests cover the parser, the priority order, the pre-scan
helper, and pin a regression for the exact "10MB" scenario in
issue #1222.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Two PRs both added CheckLinger to launchd.go, causing a redeclaration
compile error on darwin. Remove the duplicate introduced in the second PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
1. Add --dangerously-skip-permissions flag when opencode agent is in yolo
mode. Without this, headless runs auto-reject external-directory
permission requests (e.g. superpowers /init trying to modify
~/.config/opencode/), resulting in empty responses delivered to users.
2. Add CheckLinger() stub to daemon/launchd.go for macOS. This function
is only referenced from daemon.go but was only defined in
systemd.go (Linux-only build tag), causing build failure on darwin.
When cc-connect is installed as a user-level systemd service (non-root),
the service stops when the user's last login session ends (SSH disconnect)
because systemd's default Linger=no behavior.
Changes:
- Add CheckLinger function in daemon/systemd.go to check linger status
- Add warning in daemon install output when linger is disabled
- Document linger setting in INSTALL.md
The warning appears after successful installation:
⚠️ Warning: Linger is not enabled for this user.
cc-connect will stop when your last login session ends (e.g., SSH disconnect).
To keep it running persistently, run:
sudo loginctl enable-linger $USER
Fixes#960
Co-authored-by: Claude <noreply@anthropic.com>
buildPlist embeds cfg.BinaryPath, cfg.WorkDir, cfg.LogFile, and cfg.EnvPATH
into the launchd .plist via fmt.Sprintf("%s"), without escaping XML-reserved
characters. If any of those values contains '&', '<', '>', '"', or '\'',
the resulting plist is malformed XML and `launchctl bootstrap` rejects it
with a parse error, so `cc-connect daemon install` fails before the daemon
is ever registered.
The Windows daemon manager already escapes via powerShellLiteral; launchd
was the inconsistent path. Pass each user-supplied value through
xml.EscapeText. Label is a hard-coded constant and LogMaxSize is an int,
so neither needs escaping.
Concrete triggers: a workdir under `dev & test`, an iCloud-style path with
spaces and tildes already handled by the path encoder but containing '&'
or quotes, or a home directory containing an apostrophe.
PR #304 wired the LaunchAgent KeepAlive dict with SuccessfulExit=true,
intending (per its commit message) to "match systemd Restart=on-failure
behavior — only unclean exits are restarted". The semantics are
inverted: per launchd.plist(5),
SuccessfulExit <boolean>
If true, the job will be restarted as long as the program
exits with a successful exit status. If false, the job will
be restarted in the inverse condition.
so SuccessfulExit=true means "restart ONLY after a successful (exit 0)
exit", not the other way around. The shipped plist therefore had two
backwards behaviours:
1. Graceful SIGTERM shutdown — cmd/cc-connect/main.go falls through
to a clean return from main(), so the Go runtime exits 0. With
SuccessfulExit=true, launchd respawns. That is exactly the
symptom #153 was supposed to fix.
2. Crash / panic / SIGKILL — process exits non-zero. With
SuccessfulExit=true, launchd does NOT restart, so the LaunchAgent
silently goes away on the first crash and never comes back until
the user re-bootstraps it.
Flip the value to <false/> so it actually matches the stated intent
(and systemd's Restart=on-failure semantics): launchd restarts only
on non-zero exit; clean SIGTERM does not respawn.
The existing TestBuildPlist_KeepAliveDoesNotRestartOnCleanExit test
only checked that the SuccessfulExit *key* was present, not its
value, so it passed with either polarity. Tighten it to assert the
exact <false/> value with a comment explaining the inversion, so a
future edit can't silently flip it back.
* support windows daemon install
* hide windows daemon launcher window
* use scheduledtasks for windows daemon state
* stop windows task before reinstall
* wait for windows task stop before reinstall
* reuse existing windows task when protected
* register windows task for current user
* invoke schtasks executable explicitly
* address copilot windows task comments
* make windows task powershell strict
H1: Prevent workspace path traversal via ../
Add resolveLocalDirPath() that validates resolved path stays under
baseDir, expands ~/... via os.UserHomeDir, rejects escaping paths.
M2: Sanitize sender_name in inject_sender header
Replace quotes with single quotes, strip newlines/CR to prevent
prompt injection via display names.
M3: Warn on unset env var placeholders in config
resolveEnvPlaceholders now logs slog.Warn when ${VAR} references
an environment variable that is not set.
M4: Validate WeCom api_base_url
Reject non-http(s) or malformed URLs at New() to prevent credential
exfiltration via misconfigured base URL.
M5: Quote systemd Environment= values
Wrap values in double quotes to handle spaces, #, and special chars
in proxy URLs and paths.
M7: Discord table detection now requires separator row
Only wrap pipe-delimited blocks that contain a markdown separator
row (| --- | --- |), reducing false positives on decorative pipes.
Made-with: Cursor
Lowercase fmt.Errorf prefixes in daemon/update CLI paths and collapse
systemd manager error to a single line without trailing punctuation.
</think>
mcp_GitKraken_git_push
LaunchAgent used KeepAlive=true, so launchd restarted cc-connect after
every process exit, including graceful SIGTERM shutdown. That looked like
"stop then immediate restart" and could make tooling that waits for the
Unix API socket to disappear hang if the service respawned quickly.
Use KeepAlive with SuccessfulExit=true so only unclean exits are restarted,
matching systemd Restart=on-failure behavior.
Existing installs: re-run `cc-connect daemon install --force` to refresh
the plist.
Fixes#153
Made-with: Cursor
Co-authored-by: Claude <noreply@anthropic.com>
The changes add error handling for previously ignored operations and introduce logging for critical errors and warnings. This includes:
- Adding error checks for file operations, JSON unmarshaling, system calls
- Logging warnings/errors for failed operations using slog
- Properly handling return values from functions that were previously ignored
- Adding context to error messages for better debugging
- Cleaning up unused code and variables
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
When rotate() fails to open the new log file, the original code set
w.file to nil (via the failed retry that returned nil). Subsequent
Write() calls would dereference w.file and panic.
Fix:
- Add nil check in Write() to return os.ErrClosed gracefully
- Remove the useless retry in rotate() (identical call would also fail)
- Explicitly set w.file = nil on failure so the nil guard in Write() and
Close() handles it correctly
- Add support for system-level systemd (/etc/systemd/system/) when running as root
- Add requireInstalled check before start/stop/restart operations
- Improve systemd detection and error messages for WSL2, containers, SSH
- Add isWSL2() helper for better platform detection
- Update daemon help text to clarify Linux root vs non-root behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add support for running cc-connect as a daemon managed by OS init system:
- Linux: systemd user service
- macOS: launchd LaunchAgent
New CLI commands:
- cc-connect daemon install/uninstall
- cc-connect daemon start/stop/restart/status
- cc-connect daemon logs [-f] [-n N] [--log-file PATH]
Features:
- Auto log rotation with configurable max size
- Cross-platform support (systemd/launchd)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>