Add relayfile listen — stream events, run a command per match#332
Add relayfile listen — stream events, run a command per match#332khaliqgant wants to merge 10 commits into
relayfile listen — stream events, run a command per match#332Conversation
Streams live file events via WebSocket and optionally executes a shell
command per matching event, making it trivial to wire any provider
webhook to Claude or any other agent.
relayfile listen --provider linear --event file.created \
--run "claude --print 'New issue at {{path}}. Suggest priority and owner.'"
Flags:
--provider shorthand path filter: --provider linear → /linear/**
--path explicit glob filter (e.g. /linear/issues/**)
--event type filter: file.created / file.updated / file.deleted
--run shell command per event with {{path}}, {{type}},
{{provider}}, {{revision}}, {{event}} placeholders
--format text (default) or json when --run is not set
`watch` is registered as an alias for discoverability.
Help text includes concrete examples for Linear, Notion, HubSpot, Asana,
Shortcut, Granola, and Fathom, and a pointer to AgentWorkforce/factory
for teams who want the same loop running headlessly at scale.
README gains a "React to events locally" section with runnable snippets
for the most common use cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (2)
📝 WalkthroughWalkthroughAdds three new CLI subcommands: Changesrelayfile listen, dev, and supervisor subcommands
Documentation updates: README, pricing, and integration guides
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as relayfile listen
participant WS as Workspace WebSocket
participant Shell as sh -c
User->>CLI: relayfile listen --event write --run "echo {{path}}"
CLI->>WS: Connect to /v1/workspaces/{id}/fs/ws
WS-->>CLI: Stream event payloads
CLI->>CLI: Apply --provider/--path/--event filters
CLI->>Shell: Execute expanded --run command on match
Shell-->>CLI: Return exit status
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 markdownlint-cli2 (0.22.1)docs/guides/self-hosted-n8n.mdmarkdownlint-cli2 v0.22.1 (markdownlint v0.40.0) README.mdmarkdownlint-cli2 v0.22.1 (markdownlint v0.40.0) Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ad15d32d47
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| expanded := listenExpandTemplate(runCmd, evt, raw) | ||
| cmd := exec.CommandContext(rootCtx, "sh", "-c", expanded) |
There was a problem hiding this comment.
Avoid shelling raw event fields
When --run is used, the command template is expanded with raw event data and then executed via sh -c. In the documented pattern where {{path}} is embedded inside a quoted prompt, a workspace/provider path containing shell syntax such as /linear/issues/x'; touch /tmp/pwn; '.json escapes the quote and runs locally as soon as the event is received. Pass event fields through environment variables or shell-escape placeholder values before constructing the shell command.
Useful? React with 👍 / 👎.
| if typeFilter != "" { | ||
| label += " (" + typeFilter + ")" | ||
| } | ||
| fmt.Fprintf(stdout, "Listening on %s — Ctrl+C to stop\n", label) |
There was a problem hiding this comment.
Keep JSON listen output machine-readable
When --format json is selected, this unconditional banner is still written to stdout before the event stream, so the documented relayfile listen --provider linear --format json | jq '.path' fails on the non-JSON prefix. JSON mode should either suppress human status output or send it to stderr so stdout remains newline-delimited JSON.
Useful? React with 👍 / 👎.
|
Let me verify two consistency points between README/help and the code: (1) README claims One real consideration worth noting: in text output, The review is complete. Let me summarize. Review Summary — PR #332:
|
The --path flag can filter far below the provider level using the alias views already present in every provider tree: by-state/, by-label/, by-epic/, by-name/, by-id/, and more. Updated both printListenUsage and README to show concrete examples: - Linear issues filtered by state (triage, in-progress) - GitHub PRs filtered by label and specific repo - Notion edits scoped to a single database - Slack events scoped to a specific channel (incidents, etc.) - HubSpot contacts vs deals as separate paths - Asana tasks scoped to a named project - Shortcut stories filtered by epic Help text closes with a note that alias views are discoverable via 'relayfile tree / --depth 3', so users can find what's available for their own workspace without reading docs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
|
ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed. Review complete. No code changes were needed — the PR is mechanically clean (gofmt-formatted, vet-clean) and builds/tests green. Below is my review. Review: PR #332 —
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
README.md (1)
142-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the text/json output format toggle for eventless mode.
Per PR objectives, when
--runis not set, events are printed in either text or json format (default: text). The README should document the flag or mechanism for selecting output format. Additionally, the template-variable list omitseventId,contentHash,origin,correlationId, andtimestamp, which are available on FilesystemEvent but may be useful for advanced use cases.📝 Suggested documentation additions
Update line 142 to cover the missing pieces:
`--run` supports `{{path}}`, `{{type}}`, `{{provider}}`, `{{revision}}`, `{{eventId}}`, `{{contentHash}}`, `{{origin}}`, `{{correlationId}}`, `{{timestamp}}`, and `{{event}}` (full JSON). When `--run` is not set, events are printed in text format by default; pass `--json` to print raw JSON events instead. The alias views available in each provider tree (`by-state/`, `by-label/`, `by-epic/`, `by-name/`, …) are discoverable with `relayfile tree / --depth 3`.Or, if the format flag is different, adjust the wording to reflect the actual CLI surface.
To verify that the output format flag exists and works as described, please check the
relayfile listenimplementation incmd/relayfile-cli/main.goand confirm:
- The exact flag name for toggling text/json output (e.g.,
--json,--format json, etc.)- Whether all template variables listed above are actually supported.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` at line 142, Update the README.md documentation at line 142 to include the complete list of template variables supported by `--run` (currently missing eventId, contentHash, origin, correlationId, and timestamp) and add documentation for the output format toggle mechanism when `--run` is not set (e.g., the --json flag for printing raw JSON events instead of the default text format). Verify the exact flag name by checking the relayfile listen implementation in cmd/relayfile-cli/main.go to ensure the documentation accurately reflects the CLI interface.cmd/relayfile-cli/main.go (1)
40-41: 📐 Maintainability & Code Quality | 🔵 TrivialConsider updating
nhooyr.io/websocketimports togithub.com/coder/websocket.The library moved to
github.com/coder/websocketin 2024 and is no longer maintained at the original vanity import path. The API remains unchanged, but the import statement must be updated and reflected ingo.mod. This is a codebase-wide change beyond the scope of this PR.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmd/relayfile-cli/main.go` around lines 40 - 41, Replace the two import statements for the websocket library in the import block. Change "nhooyr.io/websocket" to "github.com/coder/websocket" and update "nhooyr.io/websocket/wsjson" to use the new import path from github.com/coder/websocket instead, since the library has been relocated and is now maintained at the GitHub path. This import change should also be reflected in the go.mod file to ensure the correct dependency version is fetched.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/relayfile-cli/main.go`:
- Around line 5785-5793: The listenExpandTemplate function directly interpolates
untrusted server-delivered values (evt.Path, evt.Provider, evt.Type,
evt.Revision, and raw event JSON) into the runCmd string which is then executed
via sh -c, creating a command injection vulnerability. Refactor to pass event
data as environment variables instead of string interpolation by modifying how
listenExpandTemplate works and how the exec.CommandContext call is constructed
with the expanded command. Update the command execution to use environment
variable references like $RELAYFILE_PATH instead of template placeholders like
{{path}}, and ensure printListenUsage and documentation reflect this change. If
the template API must be preserved, shell-quote every substituted value before
insertion into the command string.
---
Nitpick comments:
In `@cmd/relayfile-cli/main.go`:
- Around line 40-41: Replace the two import statements for the websocket library
in the import block. Change "nhooyr.io/websocket" to
"github.com/coder/websocket" and update "nhooyr.io/websocket/wsjson" to use the
new import path from github.com/coder/websocket instead, since the library has
been relocated and is now maintained at the GitHub path. This import change
should also be reflected in the go.mod file to ensure the correct dependency
version is fetched.
In `@README.md`:
- Line 142: Update the README.md documentation at line 142 to include the
complete list of template variables supported by `--run` (currently missing
eventId, contentHash, origin, correlationId, and timestamp) and add
documentation for the output format toggle mechanism when `--run` is not set
(e.g., the --json flag for printing raw JSON events instead of the default text
format). Verify the exact flag name by checking the relayfile listen
implementation in cmd/relayfile-cli/main.go to ensure the documentation
accurately reflects the CLI interface.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 094d597f-32a7-40fd-b876-18f17cb6ffa4
📒 Files selected for processing (2)
README.mdcmd/relayfile-cli/main.go
- `relayfile dev`: zero-friction entry point that checks auth/status then hands off to the listen loop; prints a setup prompt if not connected - `relayfile listen --background`: detaches to background, logs to ~/.relayfile/listen.log; `--daemonized` internal flag for child process - Added listenPIDFile, listenLogFile, spawnBackgroundListenProcess helpers - Wired `dev` into run() switch, printHelpForArgs, printUsage - README: documents background mode, relayfile dev, cloud webhook note (#3) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
Lead with the reactive loop — event fires, files already synced, agent acts without API calls — rather than presenting filesystem and events as separate features. Cut from 409 to 106 lines: removed evals, SDK internals, mount layout diagram, connection migration guide, verbose multi-agent section, and operational details that belong in linked docs. Sharpened the Hookdeck comparison to name the context gap explicitly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
Writes a systemd user unit (Linux) or launchd plist (macOS) so the
listen daemon survives reboots without manual --background management.
relayfile supervisor install --path "/linear/issues/by-state/triage/**" \
--event file.created --run "claude --print '...'"
relayfile supervisor status
relayfile supervisor uninstall
- systemd: ~/.config/systemd/user/relayfile-listen.service; enables and
starts via systemctl --user, RestartSec=5s on failure
- launchd: ~/Library/LaunchAgents/com.relayfile.listen.plist; loaded with
KeepAlive=true so macOS restarts it on crash and at login
- All relayfile listen flags pass through verbatim into the unit file
- shelljoin quotes args with spaces/metacharacters for ExecStart
- plistEscapeXML handles XML entities in plist string values
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
The README rewrite cut the TypeScript SDK path. Add it back as a first-class section with ensureMountedWorkspace, error types, and a link to the post-auth-mount-session guide. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
Events-based pricing: free (10k/mo), starter $49 (100k/mo), team $199 (500k/mo), enterprise custom. Covers what counts as an event, overage rates, BYO connections for enterprise, and hosted vs self-hosted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
Events alone undersells the product — relayfile also stores the full live state of every connected provider. Added storage tier (1/10/50 GB), storage overage ($0.25/GB/mo), and a plain-English explanation of what storage covers and typical usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/relayfile-cli/main.go`:
- Around line 6047-6049: The code constructs execArgs with listenArgs without
validating that forbidden flags are present, which can cause the supervisor
service to crash and restart in a loop if a user passes --background or
--daemonized. Before appending listenArgs to execArgs (before the shelljoin
call), add validation logic that checks if listenArgs contains either the
--background or --daemonized flag and returns an error with a clear message
explaining these flags are incompatible with supervisor installation. This
validation should apply to all locations where listenArgs are used to construct
the listen command in supervisor install contexts.
- Around line 5765-5769: The issue is that log file rotation happens twice: once
in the parent process before daemonizing, and again in the child when it
re-enters runListen with the daemonized flag set. This double rotation causes
the daemon's stdout/stderr file descriptor to point to a rotated file instead of
the active listen.log, breaking log visibility. Remove the rotateLogFile call in
the daemonized conditional block (the if statement checking *daemonized at lines
5765-5769) since the parent process already handles rotation before opening the
listen.log file descriptor for the child.
- Around line 6216-6225: The shelljoin function detects dollar signs as special
characters and wraps arguments in double quotes, but does not escape the dollar
signs themselves. In systemd's ExecStart, variable expansion occurs even within
double quotes, so literal dollar signs must be escaped as double dollar signs.
Modify the shelljoin function to additionally replace each dollar sign with two
dollar signs (using strings.ReplaceAll similar to how backslashes and quotes are
already being escaped) before wrapping the argument in quotes. This ensures that
when the quoted argument is processed by systemd, the dollar signs remain
literal rather than being interpreted as variable expansion specifiers.
- Around line 5918-5919: The spawnBackgroundListenProcess function passes a
--pid-file flag to the child process, but the runListen function does not
register this flag in its flag set (near line 5735) or include it in the
normalizeFlagArgs map (lines 5741-5751), causing fs.Parse() to reject it and the
child to fail immediately. Additionally, writeDaemonPIDState is never called to
write the PID to the file. Fix this by either registering the pid-file string
flag in runListen's flag set, adding it to the normalizeFlagArgs map, and
calling writeDaemonPIDState when daemonized to persist the PID; or remove the
--pid-file argument from spawnBackgroundListenProcess entirely if PID tracking
is not yet required.
In `@README.md`:
- Around line 55-56: The grep command pattern '"state":"Todo"' is searching for
minified JSON without spaces, but the JSON examples shown in the README use
pretty-printed format with spaces around colons. Update the grep pattern to
include the space after the colon to match the documented JSON format, changing
the search string from the minified version to include the space that appears in
the actual example output above it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 818dae11-90d0-4889-90c2-907e7ded0b04
📒 Files selected for processing (3)
README.mdcmd/relayfile-cli/main.godocs/guides/pricing.md
✅ Files skipped from review due to trivial changes (1)
- docs/guides/pricing.md
| if *daemonized { | ||
| if err := rotateLogFile(listenLogFile()); err != nil { | ||
| return err | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Double log rotation sends daemon output to the wrong file.
The parent already calls rotateLogFile(logFile) and opens listen.log as the child's stdout/stderr (Lines 5903, 5920-5927). When the child re-enters runListen with --daemonized, it rotates again here, renaming the freshly-opened listen.log. Because the child's stdout fd is still bound to that now-renamed file, all daemon output lands in the rotated file and ~/.relayfile/listen.log stays empty — breaking the documented log location and relayfile logs.
Rotate in exactly one place. Since the parent already rotates before opening the handle, drop the rotation in the daemonized branch.
🪵 Proposed fix
- if *daemonized {
- if err := rotateLogFile(listenLogFile()); err != nil {
- return err
- }
- }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if *daemonized { | |
| if err := rotateLogFile(listenLogFile()); err != nil { | |
| return err | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/relayfile-cli/main.go` around lines 5765 - 5769, The issue is that log
file rotation happens twice: once in the parent process before daemonizing, and
again in the child when it re-enters runListen with the daemonized flag set.
This double rotation causes the daemon's stdout/stderr file descriptor to point
to a rotated file instead of the active listen.log, breaking log visibility.
Remove the rotateLogFile call in the daemonized conditional block (the if
statement checking *daemonized at lines 5765-5769) since the parent process
already handles rotation before opening the listen.log file descriptor for the
child.
| childArgs := append([]string{"listen"}, filtered...) | ||
| childArgs = append(childArgs, "--daemonized", "--pid-file", pidFile) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Look for any pid-file flag registration and inspect normalizeFlagArgs.
rg -nP '"pid-file"|pid-file|pidFile' cmd/relayfile-cli/main.go
echo "---- normalizeFlagArgs impl ----"
ast-grep --pattern 'func normalizeFlagArgs($$$) $_ {
$$$
}'Repository: AgentWorkforce/relayfile
Length of output: 3265
🏁 Script executed:
sed -n '5730,5770p' cmd/relayfile-cli/main.goRepository: AgentWorkforce/relayfile
Length of output: 1776
🏁 Script executed:
sed -n '5900,5930p' cmd/relayfile-cli/main.goRepository: AgentWorkforce/relayfile
Length of output: 1101
🏁 Script executed:
# Check if pidFile is ever written in the listen command path
rg -n 'writeDaemonPIDState|pidFile.*Write|ioutil.WriteFile.*pidFile' cmd/relayfile-cli/main.go | head -20Repository: AgentWorkforce/relayfile
Length of output: 297
🏁 Script executed:
# Search for writeDaemonPIDState usage specifically in listen-related code
sed -n '5730,5900p' cmd/relayfile-cli/main.go | grep -n "writeDaemonPIDState\|pidFile"Repository: AgentWorkforce/relayfile
Length of output: 259
🏁 Script executed:
# Check what happens after daemonized flag in runListen
sed -n '5768,5850p' cmd/relayfile-cli/main.goRepository: AgentWorkforce/relayfile
Length of output: 2858
Background mode is broken: the daemonized child is launched with an unregistered --pid-file flag.
spawnBackgroundListenProcess re-invokes the binary with listen … --daemonized --pid-file <path>, but runListen's flag set (line 5735) does not register a pid-file string flag, and its normalizeFlagArgs map (lines 5741-5751) does not list it. When the detached child re-enters runListen, fs.Parse() (which uses flag.ContinueOnError) will reject the unknown --pid-file flag and return an error, causing the child to exit immediately — so relayfile listen --background starts a process that dies on launch. Additionally, the listen command never calls writeDaemonPIDState, so even if the process survived, nothing would be written to the PID file.
Either register a pid-file string flag in runListen, write the PID to it once daemonized, or drop the --pid-file argument from spawnBackgroundListenProcess if PID tracking is not yet implemented.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/relayfile-cli/main.go` around lines 5918 - 5919, The
spawnBackgroundListenProcess function passes a --pid-file flag to the child
process, but the runListen function does not register this flag in its flag set
(near line 5735) or include it in the normalizeFlagArgs map (lines 5741-5751),
causing fs.Parse() to reject it and the child to fail immediately. Additionally,
writeDaemonPIDState is never called to write the PID to the file. Fix this by
either registering the pid-file string flag in runListen's flag set, adding it
to the normalizeFlagArgs map, and calling writeDaemonPIDState when daemonized to
persist the PID; or remove the --pid-file argument from
spawnBackgroundListenProcess entirely if PID tracking is not yet required.
| execArgs := []string{executable, "listen"} | ||
| execArgs = append(execArgs, listenArgs...) | ||
| execStart := shelljoin(execArgs) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Reject --background/--daemonized in supervisor install flags.
Both the systemd unit (Type=simple) and launchd plist (KeepAlive) expect the listen process to stay in the foreground. If a user passes --background, the embedded relayfile listen --background detaches and exits, and the supervisor will treat the service as crashed and restart it in a loop. Consider validating listenArgs and erroring early if --background/--daemonized is present.
Also applies to: 6099-6099
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/relayfile-cli/main.go` around lines 6047 - 6049, The code constructs
execArgs with listenArgs without validating that forbidden flags are present,
which can cause the supervisor service to crash and restart in a loop if a user
passes --background or --daemonized. Before appending listenArgs to execArgs
(before the shelljoin call), add validation logic that checks if listenArgs
contains either the --background or --daemonized flag and returns an error with
a clear message explaining these flags are incompatible with supervisor
installation. This validation should apply to all locations where listenArgs are
used to construct the listen command in supervisor install contexts.
| func shelljoin(args []string) string { | ||
| quoted := make([]string, len(args)) | ||
| for i, a := range args { | ||
| if strings.ContainsAny(a, " \t\"'\\$`{}[]|&;<>()#~!") { | ||
| a = "\"" + strings.ReplaceAll(a, "\"", "\\\"") + "\"" | ||
| } | ||
| quoted[i] = a | ||
| } | ||
| return strings.Join(quoted, " ") | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor
🧩 Analysis chain
🌐 Web query:
Does systemd ExecStart expand $VAR and ${VAR} inside double-quoted arguments, and is $$ required to escape a literal dollar sign?
💡 Result:
Yes, systemd performs environment variable expansion for
Citations:
- 1: https://www.freedesktop.org/software/systemd/man/252/systemd.service.html
- 2: https://unix.stackexchange.com/questions/216780/why-does-bash-parameter-expansion-not-work-inside-systemd-service-files
- 3: https://stackoverflow.com/questions/33097284/bash-brace-expansion-in-systemd-execstart
- 4: https://serverfault.com/questions/714592/how-does-this-variable-escaping-work-in-a-systemd-unit-file
- 5: https://lists.freedesktop.org/archives/systemd-devel/2013-September/013313.html
🏁 Script executed:
# First, find the file and understand its context
fd "main.go" | grep -i relay
# Search for shelljoin usage
rg "shelljoin" -B3 -A3Repository: AgentWorkforce/relayfile
Length of output: 238
🏁 Script executed:
# Search for shelljoin usage in the codebase
rg "shelljoin" -A5 -B5
# Also check if the file has both shelljoin definition and its usages
rg "shelljoin|ExecStart" cmd/relayfile-cli/main.go -B2 -A2Repository: AgentWorkforce/relayfile
Length of output: 292
🏁 Script executed:
# Check the actual content around line 6216-6225 in the file
sed -n '6200,6250p' cmd/relayfile-cli/main.go
# Search more broadly for shelljoin definition
rg "func shelljoin" -A10
# Search for where arguments or commands are being escaped/quoted
rg "strings.ContainsAny|ReplaceAll.*\\\\\\\"|quoted" cmd/relayfile-cli/main.go -B2 -A2Repository: AgentWorkforce/relayfile
Length of output: 1808
🏁 Script executed:
# Now find where shelljoin is called/used
rg "shelljoin\(" -B5 -A5
# Get the total lines in the file to understand context
wc -l cmd/relayfile-cli/main.go
# Search for systemd-related code (ExecStart, unit files, etc)
rg -i "execstart|systemd|unit.*file|supervisor" cmd/relayfile-cli/main.go -B2 -A2 | head -100Repository: AgentWorkforce/relayfile
Length of output: 162
🏁 Script executed:
# Check the log file to see what happened
cat /tmp/coderabbit-shell-logs/shell-output-aAlZw1
# Try simpler search for shelljoin calls
grep -n "shelljoin(" cmd/relayfile-cli/main.go
# Search for ExecStart usage
grep -n "ExecStart" cmd/relayfile-cli/main.go
# Get file size first
wc -l cmd/relayfile-cli/main.goRepository: AgentWorkforce/relayfile
Length of output: 456
🏁 Script executed:
# Look at the context around line 6049 where shelljoin is called
sed -n '6030,6080p' cmd/relayfile-cli/main.go
# Also check what execArgs contains and how this ExecStart is used
sed -n '6000,6100p' cmd/relayfile-cli/main.goRepository: AgentWorkforce/relayfile
Length of output: 4713
🏁 Script executed:
# Check what listenArgs could contain and where it comes from
sed -n '5900,6000p' cmd/relayfile-cli/main.go | head -100
# Search for where listenArgs is populated in "listen" command
grep -n "func.*listen\|listenArgs\|--" cmd/relayfile-cli/main.go | head -50
# Check if there's any handling of environment variables in arguments
grep -n "\$\|environment\|ENV" cmd/relayfile-cli/main.go | head -30Repository: AgentWorkforce/relayfile
Length of output: 7981
shelljoin does not escape $ for systemd's ExecStart expansion, breaking arguments containing dollar signs.
systemd performs variable and specifier expansion on ExecStart even within double quotes; literal $ must be written as $$ to prevent expansion. When a user provides a --run argument containing $VAR or ${VAR}, shelljoin will double-quote it (detecting $ as a special character) but will not escape the $ itself. systemd will then expand the variable before the process sees it, producing a broken or unintended command. Consider escaping $ → $$ in shelljoin (or handle it specifically in supervisorInstallSystemd). The launchd path is unaffected as it passes arguments as array elements in the plist file.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/relayfile-cli/main.go` around lines 6216 - 6225, The shelljoin function
detects dollar signs as special characters and wraps arguments in double quotes,
but does not escape the dollar signs themselves. In systemd's ExecStart,
variable expansion occurs even within double quotes, so literal dollar signs
must be escaped as double dollar signs. Modify the shelljoin function to
additionally replace each dollar sign with two dollar signs (using
strings.ReplaceAll similar to how backslashes and quotes are already being
escaped) before wrapping the argument in quotes. This ensures that when the
quoted argument is processed by systemd, the dollar signs remain literal rather
than being interpreted as variable expansion specifiers.
| $ grep -l '"state":"Todo"' mount/linear/issues/*.json | ||
| ``` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the grep example match the shown JSON.
grep -l '"state":"Todo"' only matches minified JSON. The example above shows spaces around the colon, so this command will miss the documented output.
Suggested fix
- $ grep -l '"state":"Todo"' mount/linear/issues/*.json
+ $ grep -l '"state"[[:space:]]*:[[:space:]]*"Todo"' mount/linear/issues/*.json📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $ grep -l '"state":"Todo"' mount/linear/issues/*.json | |
| ``` | |
| $ grep -l '"state"[[:space:]]*:[[:space:]]*"Todo"' mount/linear/issues/*.json |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 55 - 56, The grep command pattern '"state":"Todo"' is
searching for minified JSON without spaces, but the JSON examples shown in the
README use pretty-printed format with spaces around colons. Update the grep
pattern to include the space after the colon to match the documented JSON
format, changing the search string from the minified version to include the
space that appears in the actual example output above it.
Raw webhook = diff without context. Relayfile materializes the event — the file updates before --run fires, full record plus surrounding state already on disk. Agent wakes into a complete world, not a notification. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
Full stack guide for running n8n and relayfile on your own infrastructure with no data leaving the environment. Covers relayfile Write node, Trigger node, supervisor install, and data residency guarantees. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
|
Superseded by #334 (merged). The listen command, HTTP/2 ALPN fix, wsEncodeGlob space-encoding fix, and client-side path filter are all on main via #334. The README rewrite in this branch conflicts with #331 (merged). Two docs-only files (docs/guides/pricing.md and docs/guides/self-hosted-n8n.md) could be salvaged in a follow-up PR if still wanted. |
What
Adds
relayfile listen(alias:watch) to the CLI. Connects to the workspace WebSocket event stream and optionally executes a shell command for each matching event.Flags
--provider PROVIDER--provider linearmaps to/linear/**--path GLOB/linear/issues/**--event TYPEfile.created,file.updated,file.deleted--run CMD{{path}},{{type}},{{provider}},{{revision}},{{event}}(full JSON)--format text|json--runis not set (default:text)How it works
/v1/workspaces/{id}/fs/wswithfrom=now(skips backlog, live events only)--provideror--pathis set--event--run: shells out tosh -c <expanded-cmd>per event; stdout goes to the caller's stdout, stderr to stderrScope
--runreceives clean, consistent data regardless of which SaaS tool fired the eventrelayfile help listenshows runnable examples for Linear, Notion, HubSpot, Asana, Shortcut, Granola, and Fathom, plus a pointer to AgentWorkforce/factory for teams who want the same loop running headlessly at scaleTest plan
relayfile listen --helpprints usage with examplesrelayfile listenconnects and prints events in text formatrelayfile listen --format jsonprints raw JSON per eventrelayfile listen --provider linearfilters to/linear/**events onlyrelayfile listen --event file.createdfilters by typerelayfile listen --run "echo {{path}}"executes once per matching eventgo build ./cmd/relayfile-cli/passes🤖 Generated with Claude Code
https://claude.ai/code/session_01PF9GK3mPDyWEKXpaJ8ReB9
Generated by Claude Code