Fang it! β a lightweight system tray app that turns a GitHub repo into a notification and file archiving system. No server, no extra apps, no infrastructure. Just a GitHub account, one private repo to relay monitoring data, and the GitHub iOS/Android app.
Fast, zero-config local file watching β auto git commit β push β GitHub as the auth/notification/data layer.
Fangit has three modes:
| Mode | What it does | Latency | Best for |
|---|---|---|---|
| Sync | Watches folders, auto-commits + pushes files to GitHub | ~45-60s | Archiving outputs, logs, generated files |
| Notify (dispatch) | Triggers a GitHub Action that posts an issue comment | ~30-35s | Alerts with one GitHub account |
| Notify (direct) | Posts an issue comment via API | ~20-30s | Fastest alerts (needs a second GitHub account) |
All three deliver push notifications to your phone via the GitHub mobile app.
Watches can be routed to either sync or notify mode per-entry β the same folder can archive some files to git while sending lightweight notifications for others. An optional regex filter (match) lets you only act on lines that matter.
You have a machine doing something β rendering video, running a generative music stream, processing batch jobs β and you want to know what it's doing from your phone. The existing options are either complex (Prometheus, Grafana) or require extra infrastructure (self-hosted notification servers).
Fangit uses GitHub as the entire backend: hosting, auth, iOS/Android notifications, web viewer, history, and collaboration. You get all of that for free with a GitHub account you probably already have.
Create a new private repository on GitHub (e.g. my-fangit). This is where fangit stores files and posts notifications.
Fangit uses git's built-in credential system. If you can git push from the terminal, fangit can too.
HTTPS (recommended): Generate a Personal Access Token and enter it when git prompts for a password on first push.
- Classic PAT: check
repo+workflowscopes. Can be set to never expire. - Fine-grained PAT: select your fangit repo only, grant
Contents: Read & Write+Issues: Read & Write+Actions: Read & Write. Can be set to never expire for personal repos.
SSH: Configure an SSH key as usual. Use a host alias in ~/.ssh/config if you need multiple identities.
Launch fangit. Click the tray icon β Open config.toml. Edit the config:
[general]
github_user = "yourname"
batch_interval = 60
scan_interval = 30
[repo]
url = "https://github.com/yourname/my-fangit.git"
branch = "main"
auth = "https"Add [[watch]] blocks to monitor folders and/or [[channel]] blocks for notifications. See Configuration below.
On first launch with a valid config, fangit will clone the repo and push a notify.yml GitHub Actions workflow. This workflow handles push notifications β fangit creates it automatically.
Each [[channel]] posts to a specific GitHub issue. Create issues in your repo:
- Issue #1: "Status"
- Issue #2: "Errors"
- Issue #3: "Log"
(Or whatever makes sense for your setup. Issue titles are what appear in the iOS notification.)
Install GitHub for iOS or Android. Sign in, enable push notifications. You'll receive alerts whenever fangit posts to an issue.
Config file location:
- macOS:
~/Library/Application Support/fangit/config.toml - Linux:
~/.config/fangit/config.toml
[general]
# GitHub username β used in @mentions for notifications
github_user = "yourname"
# Seconds to wait before committing file changes (30-300)
# Lower = more responsive, higher = fewer commits
batch_interval = 60
# Filesystem scan interval in seconds (10-300)
# Supplements QFileSystemWatcher for reliability
scan_interval = 30
# Tray icon style: "dot" | "logo" | "tint"
# dot = colored circle (default, minimal)
# logo = app icon with colored status dot in corner
# tint = app icon tinted entirely in status color
tray_style = "dot"[repo]
url = "https://github.com/yourname/my-fangit.git"
branch = "main"
auth = "https" # "https" or "ssh"Each [[watch]] block monitors a local folder. What happens when files change depends on the action and match fields.
[[watch]]
path_name = "SMPLR" # required β unique ID + repo subdirectory name
path = "~/SMPLR/recipes" # local folder to watch
emoji = "π΅" # prefix for commit/notification messages
extensions = ["txt", "json"] # optional β only watch these file types
action = "sync" # optional β see routing below
match = "ERROR|WARN" # optional β regex filter, see belowpath_name is important: it becomes the subdirectory in the git repo (for sync mode). Files from ~/SMPLR/recipes/ appear in the repo under SMPLR/.
For multi-machine setups, use path_name = "SMPLR/macFaux" to namespace by machine.
The action field controls what happens when files change in the watched directory:
action value |
Behaviour |
|---|---|
"sync" or omitted |
Git commit + push (default). Files archived to repo, notification via Actions. |
A [[channel]] path_name |
Skip git. Send a notification via that channel with file content. |
If action is set to a string that doesn't match any channel, it falls through to sync mode as a safe default.
The match field is an optional regex pattern. When set, fangit scans new lines added to changed files and only acts if at least one line matches the pattern.
action |
match |
Behaviour |
|---|---|---|
"sync" |
not set | Sync all changed files (default) |
"sync" |
set | Only sync files where new lines match the regex |
| channel name | not set | Notify with last new line from each changed file |
| channel name | set | Only notify when new lines match; include the matched line |
Fangit only scans new content β it tracks byte offsets per file and reads from where it left off. A 50MB log file that grows by 3 lines only scans those 3 lines.
Notifications are designed for the iOS lock screen β the most important information comes first:
Single file with match:
β "Session ended: duration 4h32m" (12 new lines, 1 match)
π’ SMPLR: stream.log
Single file without match filter:
β "ambient_set_042 started" (3 new lines)
π’ SMPLR: stream.log
Multiple files:
β "FATAL: pipe broke" (2 match across 3 files)
π’ SMPLR
stream.log (45 new lines)
error.log (2 new lines) β "FATAL: pipe broke"
The same folder can have multiple watches with different extensions routed to different behaviours:
# .txt recipe files β archive to git repo
[[watch]]
path_name = "SMPLR-recipes"
path = "/Volumes/Chainsaw/SMPLR/Stream"
emoji = "π΅"
extensions = ["txt"]
action = "sync"
# .log files β notify only when errors occur
[[watch]]
path_name = "SMPLR-errors"
path = "/Volumes/Chainsaw/SMPLR/Stream"
emoji = "π΄"
extensions = ["log"]
action = "error"
match = "ERROR|FATAL|SIGPIPE"
# .log files β notify on session start/end
[[watch]]
path_name = "SMPLR-status"
path = "/Volumes/Chainsaw/SMPLR/Stream"
emoji = "π’"
extensions = ["log"]
action = "status"
match = "Started:|Session ended:"
# Channels these route to
[[channel]]
path_name = "status"
issue = 1
emoji = "π’"
mode = "dispatch"
action = "notify"
[[channel]]
path_name = "error"
issue = 2
emoji = "π΄"
mode = "dispatch"
action = "notify"When a .txt file appears, it gets committed and pushed to git. When a .log file is updated with Started: or Session ended:, a notification fires to the status channel. When a .log file contains ERROR, the error channel fires. Normal log lines are silently ignored.
Each [[channel]] block defines a notification destination β a specific GitHub issue.
[[channel]]
path_name = "status" # required β unique channel ID
issue = 1 # GitHub issue number to post to
emoji = "π’"
mode = "dispatch" # "dispatch" or "direct" (see below)
action = "notify" # "notify" or "notify+push"
# push_dir = "~/logs" # only used with action = "notify+push"Channels can be triggered in two ways:
- From a
[[watch]]β set the watch'sactionto the channel'spath_name - From the tray menu β click Notify β channel name to send a test message
Delivery modes:
| Mode | How | Speed | Accounts needed |
|---|---|---|---|
dispatch |
Triggers a GitHub Action β github-actions[bot] posts the comment |
~30-35s | 1 (your account) |
direct |
Posts the comment directly via API | ~20-30s | 2 (see below) |
Why two modes? GitHub suppresses notifications for your own actions. When you post a comment mentioning yourself, no notification fires. The dispatch mode works around this by having the Actions bot post the comment instead. The direct mode is faster but requires a second GitHub account to be the commenter.
Setting up direct mode:
- Create a second GitHub account (e.g.
yourname-bot) - Invite it as a collaborator on your fangit repo
- Configure git credentials for the bot account (or use a PAT)
- The bot posts comments mentioning your main account β notification fires
The notify+push action: For channels where you want both a notification and supporting files (e.g. crash logs):
[[channel]]
path_name = "errors"
issue = 2
emoji = "π΄"
mode = "dispatch"
action = "notify+push"
push_dir = "~/myapp/logs" # this directory gets committed + pushedFile appears in ~/watched-folder/
β fangit detects change (QFileSystemWatcher + periodic scan)
β Debounce timer waits (batch_interval seconds)
β [if match set: scan new lines, skip if no match]
β File copied into local repo clone
β git add + commit + push
β GitHub Actions workflow fires
β github-actions[bot] posts issue comment @mentioning you
β iOS push notification
Use cases: Archiving generative outputs, collecting batch job results, backing up config changes, monitoring what a machine produces over time.
Latency: ~45-60 seconds (batch_interval + Actions runner ~15s + Apple push ~20s).
File changes in ~/watched-folder/
β fangit detects change
β FileScanner reads new bytes since last offset
β [if match set: filter lines by regex]
β MessageFormatter builds content-first message
β NotifyManager posts to channel
β iOS push notification
Notify-mode watches fire immediately β no debounce. The scan only reads new bytes appended since the last check, so even large log files are handled efficiently.
Use cases: Monitoring log output, error detection, session tracking, render completion alerts β anything where you care about what changed not just that something changed.
Latency: ~30-35s (dispatch) or ~20-30s (direct).
Tray menu β Notify β channel name
β Notification sent via channel
β iOS push notification
Use cases: Manual test notifications, status heartbeats, integration with other apps (future CLI).
Latency: ~30-35s (dispatch) or ~20-30s (direct).
| Item | Description |
|---|---|
| Status | Current state: watching, pending, pushing, error |
| Last push | Time since last successful push |
| Watch Directories | List of monitored folders with action type |
| Notify | Send a test notification to any configured channel |
| Push now | Force immediate commit + push (skip debounce timer) |
| Pause/Resume | Temporarily stop watching |
| Open git repo | Open monitoring repo in browser |
| Open config.toml | Reveal config file (creates default if missing) |
| Quit | Stop watching and exit |
Tray icon states: π’ idle, π‘ pending, π΅ pushing, π΄ error.
Tray icon style is configurable via tray_style in config: "dot" (colored circle), "logo" (app icon with status dot), or "tint" (app icon tinted in status color).
Fangit automatically creates .github/workflows/notify.yml in your repo on first launch. This single workflow handles both sync mode (triggered by push) and dispatch notify mode (triggered by workflow_dispatch). You don't need to create or edit it manually.
If you need to recreate it, delete the file from your repo and restart fangit.
- macOS 12+ or Linux (Ubuntu 22.04+)
- Qt 6.7+ (Core, Widgets, Network)
- CMake 3.20+
- Ninja
- git CLI
./scripts/ninja.sh # Debug build
./scripts/ninja.sh run # Build + launch
./scripts/ninja.sh clean # Clean build directory./scripts/ninja-release.sh
./scripts/ninja-release.sh universal # arm64 + x86_64main.cpp
βββ ConfigManager β TOML config (toml11), read/write, path resolution
βββ GitManager β git CLI wrapper (clone, add, commit, push, pull)
βββ WatcherManager β QFileSystemWatcher + configurable periodic scan, byte offset tracking
βββ CommitBatcher β Debounce + routing: sync (git) or notify (channel), uses FileScanner
βββ FileScanner β Reads new bytes from files, applies regex, returns structured results
βββ MessageFormatter β Formats scan results for notifications, commits, or any output target
βββ WorkflowManager β Auto-creates .github/workflows/notify.yml
βββ NotifyManager β GitHub API: dispatch (workflow_dispatch) + direct (issue comment)
βββ TrayManager β System tray icon, menu, status display (dot/logo/tint styles)
fangit/
βββ CMakeLists.txt
βββ config.toml # Reference config (bundled into app)
βββ macos/Info.plist # LSUIElement (menu bar only, no dock icon)
βββ fonts/FiraCode-VariableFont_wght.ttf
βββ images/
β βββ AppIcon.png|.icns
β βββ TrayIcon.png # White silhouette for tray (dark/light mode)
β βββ we-fangit.png
βββ linux/AppIcon-512.png
βββ scripts/
β βββ ninja.sh # Debug build
β βββ ninja-release.sh # Signed release build
β βββ dummyAGL.sh # Legacy Qt compatibility
β βββ png2icns.sh # Icon conversion
βββ src/
β βββ main.cpp # Entry point, version, component wiring
β βββ resources.qrc # Embedded fonts, icons, default config
β βββ ConfigManager.h/.cpp # TOML config, tray style, watch/channel/match parsing
β βββ TrayManager.h/.cpp # System tray: dot/logo/tint icon styles
β βββ WatcherManager.h/.cpp # QFileSystemWatcher + periodic scan + byte offset tracking
β βββ GitManager.h/.cpp # git CLI wrapper
β βββ CommitBatcher.h/.cpp # Debounce, sync/notify routing, match-based filtering
β βββ FileScanner.h/.cpp # File diff reader, regex matching, structured scan results
β βββ MessageFormatter.h/.cpp # Content-first message formatting for all output targets
β βββ WorkflowManager.h/.cpp # Auto-creates GitHub Actions workflow
β βββ NotifyManager.h/.cpp # GitHub API: dispatch + direct modes
βββ third_party/toml11/ # Vendored, header-only
βββ notes/ # Design docs + test scripts
- Shells out to git CLI β inherits system auth (Keychain, SSH agent, credential helpers). No libgit2 dependency.
- toml11 vendored β header-only C++ TOML parser. No runtime or build-time network dependency.
- Append-only by design β watches for new/modified files, ignores deletes.
- Single workflow file β handles both push-triggered and dispatch-triggered notifications.
- Watchβchannel routing β
actionfield on watches connects to channels by name. No special syntax, just matching strings. - Byte offset scanning β FileScanner tracks where it last read each file. Only new content is scanned, even for large log files.
- Content-first notifications β MessageFormatter puts the most important line first, optimised for iOS lock screen banners.
- Regex match filtering β optional
matchfield works with both sync and notify. Only act when something meaningful happens. - LSUIElement β menu bar only, no dock icon (macOS).
- Cross-platform β Qt6 with QFileSystemWatcher (fsevents on macOS, inotify on Linux), QSystemTrayIcon for tray.
Do I need a second GitHub account? No. Dispatch mode works with a single account. Direct mode is faster but requires a second account because GitHub doesn't notify you about your own actions.
Will this violate GitHub's terms of service? No. Automated commits with small files at reasonable intervals (30-300s) are within GitHub's acceptable use policy. Machine accounts are explicitly permitted.
How much does this cost? Nothing. GitHub free tier includes private repos, Actions minutes, and the mobile app.
Will the repo get huge? Eventually, if you push a lot. For text files and small images, a year of use is fine. For large files, consider Git LFS or periodic history cleanup.
What if my machine goes offline? Sync mode queues commits locally. When the network returns, the next push sends everything. Notify mode requires network β missed notifications are not queued.
Can I watch the same folder with different actions?
Yes. Use multiple [[watch]] blocks with the same path but different extensions and action values. For example, .txt files sync to git while .log files trigger a notification.
What about large log files? FileScanner tracks byte offsets per file. It only reads new bytes appended since the last scan. A 50MB log file that grows by 3 lines only scans those 3 lines.
Can I filter which log lines trigger notifications?
Yes. Set match = "ERROR|FATAL|WARN" (or any regex) on a [[watch]] block. Only lines matching the pattern will trigger the action. Unmatched changes are silently ignored.
Can I use this on Linux? Yes. Qt6 provides QFileSystemWatcher (inotify backend) and QSystemTrayIcon. Build as an AppImage for distribution.
- fangit CLI with hooks for external apps & processes
- onboarding 'wizard' dialogs for people who have a github account but otherwise no clue
- various levels of 'courtesy' git options to keep monitoring repo clean
- GitLab, Bitbucket, and Codeberg integration (soon / urgent)
MIT / TBC
