{"id":51484027,"url":"https://github.com/nnao45/nixx","last_synced_at":"2026-07-07T04:30:36.447Z","repository":{"id":364436792,"uuid":"1267905491","full_name":"nnao45/nixx","owner":"nnao45","description":"Write real shell, JavaScript, Python, and TypeScript inside Nix — without escaping ${}.","archived":false,"fork":false,"pushed_at":"2026-06-26T03:28:13.000Z","size":537,"stargazers_count":7,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-26T04:05:16.805Z","etag":null,"topics":["development","development-environment","nix"],"latest_commit_sha":null,"homepage":"https://nnao45.github.io/nixx","language":"Nix","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nnao45.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-13T00:51:45.000Z","updated_at":"2026-06-26T02:06:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nnao45/nixx","commit_stats":null,"previous_names":["nnao45/nixx"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nnao45/nixx","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnao45%2Fnixx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnao45%2Fnixx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnao45%2Fnixx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnao45%2Fnixx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nnao45","download_url":"https://codeload.github.com/nnao45/nixx/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nnao45%2Fnixx/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35215221,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["development","development-environment","nix"],"created_at":"2026-07-07T04:30:35.520Z","updated_at":"2026-07-07T04:30:36.423Z","avatar_url":"https://github.com/nnao45.png","language":"Nix","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nixx — raw shell in Nix: escape-light *and* statically checked.\n\n[![release](https://img.shields.io/github/v/release/nnao45/nixx?logo=github\u0026label=release\u0026color=5277C3)](https://github.com/nnao45/nixx/releases/latest)\n[![nix flake check](https://github.com/nnao45/nixx/actions/workflows/nix-version-compat.yml/badge.svg)](https://github.com/nnao45/nixx/actions/workflows/nix-version-compat.yml)\n[![Nix ≥ 2.18](https://img.shields.io/badge/nix-%E2%89%A52.18-5277C3?logo=nixos)](https://nixos.org/download/)\n\nnixx stands on **two pillars that need each other**:\n\n1. **Escape-light.** One `with`, and a `${VAR}` in a script body is the\n   **shell's**, not Nix's — read from source, never escaped. No preprocessor, no\n   codegen; files stay valid `.nix`, so nil/nixd never error.\n2. **Statically checked.** That raw embedded shell would otherwise be opaque to\n   your tools. `shellint` reads it back — running shellcheck on the body **and**\n   guarding the one boundary the escape-light path leaves behind: the few `${…}`\n   forms that *do* still need an `''` escape, and bare `${VAR}` that needs a\n   `with`. Skip the escaping; let shellint point at whatever you missed.\n\n```nix\n{\n  packages = with inputs.nixx.lib.for pkgs; mkApps { packages = [ pkgs.rsync ]; } {\n    deploy = bash ''\n      echo \"deploying from ${HOME}\"          # ${} is shell's — no ''${ } escape\n      rsync -a ./dist/ \"$HOST:/srv/\"\n    '';\n  };\n}\n```\n```sh\nnix run .#deploy            # ships a /nix/store/.../bin/deploy\nnix run nixx#shellint -- .  # lint the embedded shell: boundary + shellcheck + env\n```\n\nSwap `mkApps` for `mkTasks` when you want a tab-completed `tasks build` /\n`tasks check` workflow.\n\n**Add to your flake:**\n```nix\ninputs.nixx.url = \"github:nnao45/nixx\";\n# then, in a per-system output with `pkgs` in scope:\n#   { packages = with inputs.nixx.lib.for pkgs; mkApps { } { hello = bash ''echo hi''; }; }\n```\n\n\u003e Also speaks python (uv), typescript (bun/tsx), node, deno, perl, ruby, lua —\n\u003e bundles dev workflows with `mkTasks`, and runs those same bodies as **hermetic\n\u003e sandbox tests** with `mkTests` (below). Full reference, dependency wiring, and\n\u003e every option table in **[API.md](./API.md)**.\n\n## Pillar 1 — `${}` belongs to the language\nEvery body passed as an attr value to `mkApps`, `mkTasks`, or `mkScripts` is read\n**from source** instead of evaluated, so a `${VAR}` in the `${}` family — shell\n`${HOME}`, a JS template `` `${x}` ``, a perl `${name}` — survives verbatim with\n**no `''` prefix**. The one line of ceremony is the `with`\n(`inputs.nixx.lib.for pkgs` — the canonical entry point): any `with` makes the\nscope dynamic, deferring Nix's static undefined-variable check to a runtime that\nnever arrives (the body is never forced). To splice in an actual **Nix** value,\nuse the `@nix(x)` / `@sh:q(x)` markers — native Nix `${…}` does not run in a\nsource-read body, by design: `${}` belongs to the language.\n\nThe common shell forms work with zero prefix. A few aren't valid Nix inside\n`${…}`, so Nix rejects them at *parse* time and they still need the `''`:\n\n| form | zero prefix? | example |\n|---|---|---|\n| `${VAR}`, `$VAR`, `$@`, `$?` | ✅ | `echo ${HOME}` |\n| `${VAR:-d}` `${VAR:=d}` `${VAR:?e}` `${VAR:+x}` and the colon-less `${VAR-d}` `${VAR+x}` `${VAR?e}` | ✅ | `echo ${EDITOR:-vi}` |\n| `${VAR:off:len}` (substring) | ✅ | `echo ${name:0:3}` |\n| `${!ref}` (indirect) `${ARR[0]}` / `${ARR[i]}` (numeric **or** variable index) | ✅ | `echo ${ARR[i]}` |\n| `${VAR/old/new}` `${VAR//old/new}` (operands identifier-only) | ✅ | `echo ${PATH//bin/BIN}` |\n| `${ARR[@]}` `${#VAR}` `${VAR%x}` `${VAR#x}` `${VAR^^}` `${VAR,,}` `${ARR[-1]}` | ❌ use `''` | `for x in ''${ARR[@]}; do …` |\n| `${VAR/o/n}` whose operand carries `, ; # %` etc. | ❌ use `''` | `''${csv/,/;}` |\n\nThe escape-light path is **partial by design** — and that's exactly the gap\nPillar 2 closes: **forget an `''` on a ❌ row, or write a bare `${VAR}` with no\n`with`, and `shellint` points at the precise `file:line`** instead of leaving you\na cryptic Nix parse error. (Mechanism — lazy thunks, `unsafeGetAttrPos`, the\nparse-wall rule — in [API.md](./API.md).)\n\n**Hate even that?** The `❌` rows exist because Nix's *parser* rejects those forms\ninside a string (`#` opens a comment, `[@]`/`%`/`^` aren't operators) — it's the\nNix grammar, not nixx. The escape hatch is **`rawsh`**: put the body in `#|`\nline-comments, which Nix never parses, so **every** parse-wall form survives with\n**zero** escaping (and no `with`):\n\n```nix\nprocess = rawsh;\n  #| for f in ${FILES[@]}; do echo \"${#f}: ${f^^} ${f%.txt}\"; done\n```\n\nThe trade is the body is a comment, so editors/`shellint` don't see it as shell.\nReach for it only when a block is dense with `❌` forms; keep `bash ''…''` (lint +\nhighlighting) for everything else.\n\n\u003e **Trade-off.** Any `with` defers undefined-variable checking, so a typo in\n\u003e *never-evaluated* code under its scope won't be caught statically — which is\n\u003e the other reason `shellint` exists. Keep the `with` on the flake output that\n\u003e builds your tasks; evaluated code still errors clearly at runtime.\n\n## Pillar 2 — `shellint`: check the embedded shell\n`shellint` is a **source-driven** linter (no eval): it parses your `.nix` files\nwith tree-sitter-nix, finds every `bash ''…''` block, and runs three passes that\na generic Nix linter can't — because only nixx knows the string is shell:\n\n- **nix-boundary** (fatal) — the warden of Pillar 1's gap. A shell-only\n  expansion that breaks Nix (`${#x}`, `${x[@]}`, `${x^^}` …) needs `''${…}`; a\n  bare `${VAR}` with no enclosing `with` will fail Nix eval. Escaped `''${…}`,\n  `with`-scoped `${VAR}`, and real Nix interpolations (`${pkgs.hello}`) are left\n  alone. tree-sitter-nix pinpoints the breakage even through parse cascades.\n- **shellcheck** (fatal) — the bash body is shellcheck'd (`$out`/`$src`-style\n  build-env refs excluded; add codes with `excludeShellChecks`).\n- **env** (warn) — lists the external env each block requires (block-bound names\n  subtracted).\n\n```sh\nnix run nixx#shellint -- ./           # lint a tree (or files)\nnix run nixx#shellint -- --fix ./     # auto-fix the boundary: add/remove '' as needed\nnix run nixx#shellint -- --fix --dry-run ./   # preview the fixes as a diff\n```\n\n`--fix` is the bidirectional normalizer for the boundary: it **escapes** a\nshell-only `${…}` that needs it (`${#x}` → `''${#x}`) and **de-escapes** an `''${VAR}`\nthat doesn't (back to `${VAR}`, only under a `with`). Each fixed file is re-parsed\nafterward and reverted if it isn't clean — so a fix can never leave you worse off.\n```nix\n# gate it in your own flake\nchecks.shellint = (inputs.nixx.lib.for pkgs).shellint {\n  src = ./.;\n  exclude = [ \"*/vendor/*\" ];\n  passes = { envcheck = false; };     # toggle individual passes\n};\n```\n\nFull pass semantics, config, and the runtime sibling `envCheck` (a `mkTasks`\noption that blocks a task when a required env var is unset) are in\n**[API.md](./API.md)**.\n\n## Per-block options\nCall a block like a function with an attrset of options. This **one idiom**\ncovers both `mkApps` (language opts like `compile`) and `mkTasks` (task opts like\n`deps` / `env` / `cwd`) — there is no separate `app` / `task` wrapper:\n\n```nix\nwith inputs.nixx.lib.for pkgs;\nmkApps { packages = [ pkgs.curl pkgs.jq ]; } {   # ← packages is global (whole set)\n  fetch  = bash ''curl -s https://api.example.com | jq .'';   # ← no opts needed\n  report = uv ''\n    from rich import print\n    print(\"[green]ok[/]\")\n  '' { requirements = [ \"rich\u003e=13\" ]; };          # ← per-block\n  check  = bun ''\n    const r: { ok: boolean } = { ok: true };\n    console.log(`status: ${r.ok}`);\n  '' { compile = true; };                         # ← per-block\n}\n```\n\nOne rule: **`packages` is global** (first attrset of `mkApps` / `mkTasks`); the\nlanguage options are **per-block**. Passing `packages` per-block throws, on purpose.\n\n| option | level | what it does |\n|---|---|---|\n| `packages` | **global** | `/bin` on PATH for **every** app/task |\n| `inputsFrom` | **global** (`mkTasks`) | other derivations' setup hooks + build inputs in the dev shell (tools that need a stdenv hook, not just a binary) |\n| `requirements` | per-block (uv) | PEP 723 inline deps |\n| `compile` | per-block (bun) | `bun --compile` → standalone binary |\n| `projectRoot` | per-block (uv/bun) | deps from `./pyproject.toml` / `package.json` |\n| `envCheck` | **global** or per-block (bash) | block a task when a *required* env var is unset/empty; `--env-list \u003ctask\u003e` just prints what it needs. Full semantics → [API.md](./API.md) |\n\nThe same call form attaches task options in `mkTasks`\n(`bash ''…'' { deps = [ … ]; env = { … }; cwd = ./d; }`). Full option matrix,\n`mkScript(s)`, and `vars` markers are in **[API.md](./API.md)**.\n\n## Task runner\n`mkTasks` is a `just`-style runner: one `tasks \u003cname\u003e` is a **single bash\nprocess**, so an `export` (or `defaultDeps`/`env`) in an early task persists into\nevery later one. **Only env crosses task boundaries** — cwd and shell options\nreset at each task's entry (every bash task is `set -euo pipefail`; a dep's `cd`\nor `set +u` can't leak), so tasks stay predictable. Supports `deps`, `group`,\nper-task `cwd` / `env`.\n\n```\n$ tasks --list\n  build    Build the project\n  test     Run the test suite\n\nrelease:\n  deploy   Deploy production\n\n$ tasks build      # or: nix run .#tasks -- build\n```\n\n**One trap:** if a task calls a tool, put it in `mkTasks { packages }`, never only\nin `mkShell` — a `mkShell`-only package is absent from `nix run .#tasks`. Full\n`env`/`deps` semantics and the `writers.mkTasks` return value (`runner` /\n`devShell` / `extendShell`) in **[API.md](./API.md)**.\n\n## Hermetic tests — `mkTests`\n`mkTests` runs each test in the **Nix build sandbox**: no `$HOME`, no network, and\nPATH limited to the suite's declared `packages`. A green run isn't \"passed on my\nmachine\" — it's \"passed with *only* what was declared, offline, against a freshly\nminted `$HOME`.\" A hidden `curl`, a stray `~/.aws/credentials` read, or an\nundeclared `jq` can't sneak a pass: the sandbox withholds them and the test fails\nthe instant it reaches for one. That guarantee is what a host-resident runner like\nbats can't give you.\n\nBodies are the usual source-read `bash ''…''` (escape-light, shellcheck'd), with a\n**bats-compatible** vocabulary so existing muscle memory transfers — over a\nper-test writable `$WORK` the harness mints and tears down:\n\n```nix\nwith inputs.nixx.lib.for pkgs;\n{\n  # one mkTests call = one suite = one named check\n  checks.${system}.tools = mkTests { name = \"tools\"; packages = [ pkgs.jq ]; } {\n    setup = bash ''mkdir -p \"$WORK/out\"'';            # lifecycle hook, not a test\n\n    \"jq extracts fields\" = bash ''\n      run jq -n '{ port: 8080 }'                      # run captures $status/$output\n      assert_success\n      assert_json '.port' '8080'\n    '';\n\n    \"writes into the per-test $WORK\" = bash ''\n      printf 'ok' \u003e \"$WORK/out/marker\"\n      assert_file \"$WORK/out/marker\"\n      assert_file_contains \"$WORK/out/marker\" ok\n    '';\n  };\n}\n```\n\n`run` captures `$status` / `$output` / `$stderr` (bats style); `assert_success`,\n`assert_failure`, `assert_output [--partial|--regexp]`, `refute_output`, and\n`assert_equal` are joined by nixx extras `assert_file`, `assert_dir`,\n`assert_file_contains`, and `assert_json`. `setup` / `teardown` / `setup_suite` /\n`teardown_suite` are lifecycle hooks. The code under test is reached by name if\nit's a nixx app, or via `src = ./.;` (mounted read-only as `$FIXTURES`).\n\n**Two lanes from one definition:**\n\n| lane | how | when |\n|---|---|---|\n| **hermetic** | the returned derivation → `checks.\u003csystem\u003e`; `nix flake check` builds it in the sandbox | CI, pre-commit — the source of truth |\n| **fast** | `.fast` — a devShell-lane runnable, no sandbox, instant | the carve-carve TDD loop |\n\n**`nixx test`** (shipped as a flake app: `nix run nixx#test -- …`) discovers\n`*_test.nix` and drives both lanes:\n\n```sh\nnix run nixx#test                 # fast lane: every *_test.nix under .\nnix run nixx#test -- -f deploy     # only tests whose name contains \"deploy\"\nnix run nixx#test -- --hermetic    # run each suite in the sandbox\nnix flake check                   # the hermetic suites, as checks\n```\n\nWhen a hermetic test goes red, **`--repro`** drops you into that test's *exact*\nenvironment — same `$WORK`, same PATH, `setup` already run, every `run`/`assert_*`\nhelper loaded — as an interactive shell; type `t` to re-run the body and watch the\nassertion fail live:\n\n```sh\nnix run nixx#test -- path/to/x_test.nix --repro \"jq extracts fields\"\n#   cwd is $WORK (writable) · helpers loaded · `t` runs the body · Ctrl-D leaves\n```\n\n`--once` reads stdin one-shot instead of opening a prompt, so the repro path is\nscriptable and CI-able.\n\n\u003e **Eating its own shell.** The assert runtime and `nixx test` CLI are written as\n\u003e source-read `bash ''…''` inside nixx itself — no external `.sh`. Runnable suites\n\u003e live in `tests/*_test.nix`; the full option/assert reference is in\n\u003e **[API.md](./API.md)**.\n\n## Process groups with process-compose\nUse `processCompose` when the workflow is several long-running processes instead\nof a dependency-ordered task list. It turns nixx bash blocks into a\n`process-compose` config with readiness gates, `depends_on`, restart policy, and\ngraceful shutdown:\n\n```nix\nwith inputs.nixx.lib.for pkgs;\nlet\n  pc = processCompose {\n    name = \"dev\";\n    vars = { port = 3000; };\n    \"no-server\" = true;\n  } {\n    web = bash ''\n      echo \"web on @nix(port), home=${HOME}\"\n      sleep 30\n    '' { readiness = { exec = \"true\"; initial_delay_seconds = 2; }; };\n\n    api = bash ''\n      echo \"api after web\"\n      sleep 30\n    '' { depends_on = [ \"web\" ]; restart = \"on_failure\"; };\n  };\nin {\n  packages.default = pc.runner;       # nix run .#default\n}\n```\n\nPer-process options use the same `bash ''...'' { ... }` call form as tasks:\n`cwd`, `env`, `depends_on`, `readiness`, `restart`, `description`, `namespace`,\nand `shutdown`. `pc.config`, `pc.configJson`, and `pc.configFile` expose the\ngenerated process-compose config for inspection or reuse. Runner options also\nexpose process-compose global flags like `\"no-server\"`, `\"use-uds\"`, and `port`.\nFull mapping and return values are in **[API.md](./API.md)**; the runnable\nexample is `examples/process-compose`.\n\n## Dev shells — pick your idiom\nSame `with inputs.nixx.lib.for pkgs;`, same zero-`${}`-tax bodies; only the wiring\ndiffers. The zero-config path lands the runner on PATH, tab-completed:\n\n```nix\nwith inputs.nixx.lib.for pkgs;\nlet\n  apps  = mkApps  { }                 { whereami = bash ''echo ${PWD} as ${USER}''; };\n  tasks = mkTasks { name = \"tasks\"; } { info     = bash ''echo ${PWD} as ${USER}''; };\nin {\n  packages = apps // { default = tasks.runner; tasks = tasks.runner; };\n  devShells.default = tasks.devShell;\n}\n```\n\n`tasks.extendShell` folds the runner into your own `pkgs.mkShell` (preserving its\nenv + shellHook); `shellHook` / `runCommand` wrappers give the same `${}`-tax-free\nbodies to plain Nix APIs; and devenv is supported by feeding a body's `.text` into\n`enterShell`. Runnable flakes for all three live in `examples/`; the wiring is\ndocumented in **[API.md](./API.md)**.\n\n## More\n- **Docs site**: [nnao45.github.io/nixx](https://nnao45.github.io/nixx) — browsable reference with examples.\n- **LLM-friendly plain-text**: [nnao45.github.io/nixx/llms.txt](https://nnao45.github.io/nixx/llms.txt)\n- **Multi-language \u0026 shippable binaries**, `mkScript(s)`, `vars` markers\n  (`@nix`, `@sh:q`), full language/option tables, `shellint` \u0026 `envCheck`\n  reference, `processCompose`, and the linter source-mapping:\n  **[API.md](./API.md)**.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnnao45%2Fnixx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnnao45%2Fnixx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnnao45%2Fnixx/lists"}