[{"data":1,"prerenderedAt":572},["ShallowReactive",2],{"blog-/blog/reduce-supply-chain-risk-with-recul":3},{"id":4,"title":5,"body":6,"date":560,"description":561,"extension":562,"meta":563,"navigation":132,"path":564,"published":132,"seo":565,"stem":566,"tags":567,"__hash__":571},"blog/blog/reduce-supply-chain-risk-with-recul.md","Reduce Supply Chain Risk With Recul",{"type":7,"value":8,"toc":553},"minimark",[9,24,34,39,90,95,98,146,149,157,160,164,171,405,416,422,426,431,495,498,502,512,549],[10,11,12,13,23],"p",{},"Recently I released ",[14,15,16],"strong",{},[17,18,22],"a",{"href":19,"rel":20},"https://github.com/CRBroughton/recul",[21],"nofollow","Recul",", a CLI tool for npm and pnpm repositories that helps reduce supply chain risk by keeping your dependencies deliberately behind the latest release.",[10,25,26,28,29,33],{},[14,27,22],{}," is not a replacement for ",[30,31,32],"code",{},"npm audit"," or any third-party security tooling, it's a complementary layer on top of them, one that reduces your attack surface without requiring active attention. Drop it into CI, commit the configuration file, and your team now has a clear, auditable policy with a pass/fail signal on every pipeline run.",[35,36,38],"h3",{"id":37},"what-recul-gives-you","What Recul gives you",[40,41,42,49,55,61,67,77],"ul",{},[43,44,45,48],"li",{},[14,46,47],{},"Auditable, Configuration-based lag policy",": define how many versions behind latest you want to stay, committed to your repository, giving team alignment and an auditable policy",[43,50,51,54],{},[14,52,53],{},"Pass/fail CI integration",": ships as a GitHub Action; fails the build when anything drifts ahead of the lag target",[43,56,57,60],{},[14,58,59],{},"Exact install commands",": any violating package comes with the precise command to bring it back in line",[43,62,63,66],{},[14,64,65],{},"Defence-in-depth controls",": combine version lag with a minimum release age so even fast-releasing packages have to wait",[43,68,69,72,73,76],{},[14,70,71],{},"pnpm monorepo & catalog support",": audits each workspace package separately, with ",[30,74,75],{},"--fix"," to apply catalog updates in one go",[43,78,79,82,83,86,87],{},[14,80,81],{},"Lockfile-aware",": reads installed versions from your lockfile for accurate results on packages declared with ",[30,84,85],{},"^"," or ",[30,88,89],{},"~",[91,92,94],"h2",{"id":93},"getting-started","Getting started",[10,96,97],{},"First generate the Recul policy configuration file, then run your first audit:",[99,100,105],"pre",{"className":101,"code":102,"language":103,"meta":104,"style":104},"language-bash shiki shiki-themes vitesse-black","# Create a config file in the current directory\nrecul init\n\n# Audit your dependencies\nrecul\n","bash","",[30,106,107,116,127,134,140],{"__ignoreMap":104},[108,109,112],"span",{"class":110,"line":111},"line",1,[108,113,115],{"class":114},"sux-A","# Create a config file in the current directory\n",[108,117,119,123],{"class":110,"line":118},2,[108,120,122],{"class":121},"sCK9x","recul",[108,124,126],{"class":125},"s7rlk"," init\n",[108,128,130],{"class":110,"line":129},3,[108,131,133],{"emptyLinePlaceholder":132},true,"\n",[108,135,137],{"class":110,"line":136},4,[108,138,139],{"class":114},"# Audit your dependencies\n",[108,141,143],{"class":110,"line":142},5,[108,144,145],{"class":121},"recul\n",[10,147,148],{},"Once run, you'll get a clear table of where each dependency stands against your lag target:",[99,150,155],{"className":151,"code":153,"language":154},[152],"language-text","recul  staying 2 versions behind latest\n\nsettings\nsetting    value   description\n──────────────────────────────────────────────────────────────────\nlag        2       stay 2 versions behind latest\npm         pnpm    the chosen package manager\nbehind     ignore  ignore packages behind target\nrange      exact   pin exact versions\nminAge     3       skip versions published within the last 3 days\nsameMajor  true    restrict candidates to current major\n\npackage    declared  → target  installed  latest  gap  status\n────────────────────────────────────────────────────────────────\nexpress    ^4.19.2   4.17.3    4.17.3     4.19.2  2    ↓ will pin back\nreact      ^18.3.1   18.1.0    18.0.0     18.3.1  2    ↓ will pin back\ntypescript 5.4.5     5.4.5     5.4.5      5.4.5   0    ✓ ok\n\nto pin back:\n  pnpm add express@4.17.3 react@18.1.0\n","text",[30,156,153],{"__ignoreMap":104},[10,158,159],{},"Any package ahead of your lag target gets flagged, and Recul generates the exact install command to bring it back in line.",[91,161,163],{"id":162},"configuration","Configuration",[10,165,166,167,170],{},"Commit a ",[30,168,169],{},"recul.config.jsonc"," to standardise the policy across your team:",[99,172,176],{"className":173,"code":174,"language":175,"meta":104,"style":104},"language-jsonc shiki shiki-themes vitesse-black","{\n  // How many versions to stay behind the latest published release.\n  // Counted in releases, not semver increments.\n  //\n  //   1  →  days to weeks   (fast-moving projects, minimal buffer)\n  //   2  →  weeks           (balanced default, recommended)\n  //   3  →  weeks to months (cautious teams, slower release cadences)\n  //   5+ →  months          (regulated environments, high-security contexts)\n  \"lag\": 2,\n\n  // Package manager: \"npm\" | \"pnpm\"\n  \"packageManager\": \"pnpm\",\n\n  // Path to the package.json to audit, relative to this config file.\n  \"packageFile\": \"package.json\",\n\n  // How to handle packages already older than the lag target.\n  //   \"ignore\"  →  treat as ok, no output (default)\n  //   \"report\"  →  surface them with a safe upgrade-to-target command\n  \"behindBehavior\": \"ignore\",\n\n  // Version prefix used in generated install commands.\n  //   \"exact\"  →  1.3.4    (recommended; audits are reliable)\n  //   \"caret\"  →  ^1.3.4   (allows minor/patch drift)\n  //   \"tilde\"  →  ~1.3.4   (allows patch drift only)\n  \"rangeSpecifier\": \"exact\",\n\n  // Packages to skip entirely.\n  \"ignore\": [],\n\n  // Minimum days a version must have been published before it is eligible\n  // as a lag target. Combines with \"lag\" for defence-in-depth.\n  \"minimumReleaseAge\": 3,\n\n  // Pre-release identifiers to exclude from the candidate list.\n  \"preReleaseFilter\": [\"-alpha\", \"-beta\", \"-rc\", \"-next\", \"-canary\", \"-dev\"],\n\n  // Restrict candidates to the same major as the currently declared version.\n  \"sameMajor\": true\n}\n","jsonc",[30,177,178,183,188,193,198,203,209,215,221,227,232,238,244,249,255,261,266,272,278,284,290,295,301,307,313,319,325,330,336,342,347,353,359,365,370,376,382,387,393,399],{"__ignoreMap":104},[108,179,180],{"class":110,"line":111},[108,181,182],{},"{\n",[108,184,185],{"class":110,"line":118},[108,186,187],{},"  // How many versions to stay behind the latest published release.\n",[108,189,190],{"class":110,"line":129},[108,191,192],{},"  // Counted in releases, not semver increments.\n",[108,194,195],{"class":110,"line":136},[108,196,197],{},"  //\n",[108,199,200],{"class":110,"line":142},[108,201,202],{},"  //   1  →  days to weeks   (fast-moving projects, minimal buffer)\n",[108,204,206],{"class":110,"line":205},6,[108,207,208],{},"  //   2  →  weeks           (balanced default, recommended)\n",[108,210,212],{"class":110,"line":211},7,[108,213,214],{},"  //   3  →  weeks to months (cautious teams, slower release cadences)\n",[108,216,218],{"class":110,"line":217},8,[108,219,220],{},"  //   5+ →  months          (regulated environments, high-security contexts)\n",[108,222,224],{"class":110,"line":223},9,[108,225,226],{},"  \"lag\": 2,\n",[108,228,230],{"class":110,"line":229},10,[108,231,133],{"emptyLinePlaceholder":132},[108,233,235],{"class":110,"line":234},11,[108,236,237],{},"  // Package manager: \"npm\" | \"pnpm\"\n",[108,239,241],{"class":110,"line":240},12,[108,242,243],{},"  \"packageManager\": \"pnpm\",\n",[108,245,247],{"class":110,"line":246},13,[108,248,133],{"emptyLinePlaceholder":132},[108,250,252],{"class":110,"line":251},14,[108,253,254],{},"  // Path to the package.json to audit, relative to this config file.\n",[108,256,258],{"class":110,"line":257},15,[108,259,260],{},"  \"packageFile\": \"package.json\",\n",[108,262,264],{"class":110,"line":263},16,[108,265,133],{"emptyLinePlaceholder":132},[108,267,269],{"class":110,"line":268},17,[108,270,271],{},"  // How to handle packages already older than the lag target.\n",[108,273,275],{"class":110,"line":274},18,[108,276,277],{},"  //   \"ignore\"  →  treat as ok, no output (default)\n",[108,279,281],{"class":110,"line":280},19,[108,282,283],{},"  //   \"report\"  →  surface them with a safe upgrade-to-target command\n",[108,285,287],{"class":110,"line":286},20,[108,288,289],{},"  \"behindBehavior\": \"ignore\",\n",[108,291,293],{"class":110,"line":292},21,[108,294,133],{"emptyLinePlaceholder":132},[108,296,298],{"class":110,"line":297},22,[108,299,300],{},"  // Version prefix used in generated install commands.\n",[108,302,304],{"class":110,"line":303},23,[108,305,306],{},"  //   \"exact\"  →  1.3.4    (recommended; audits are reliable)\n",[108,308,310],{"class":110,"line":309},24,[108,311,312],{},"  //   \"caret\"  →  ^1.3.4   (allows minor/patch drift)\n",[108,314,316],{"class":110,"line":315},25,[108,317,318],{},"  //   \"tilde\"  →  ~1.3.4   (allows patch drift only)\n",[108,320,322],{"class":110,"line":321},26,[108,323,324],{},"  \"rangeSpecifier\": \"exact\",\n",[108,326,328],{"class":110,"line":327},27,[108,329,133],{"emptyLinePlaceholder":132},[108,331,333],{"class":110,"line":332},28,[108,334,335],{},"  // Packages to skip entirely.\n",[108,337,339],{"class":110,"line":338},29,[108,340,341],{},"  \"ignore\": [],\n",[108,343,345],{"class":110,"line":344},30,[108,346,133],{"emptyLinePlaceholder":132},[108,348,350],{"class":110,"line":349},31,[108,351,352],{},"  // Minimum days a version must have been published before it is eligible\n",[108,354,356],{"class":110,"line":355},32,[108,357,358],{},"  // as a lag target. Combines with \"lag\" for defence-in-depth.\n",[108,360,362],{"class":110,"line":361},33,[108,363,364],{},"  \"minimumReleaseAge\": 3,\n",[108,366,368],{"class":110,"line":367},34,[108,369,133],{"emptyLinePlaceholder":132},[108,371,373],{"class":110,"line":372},35,[108,374,375],{},"  // Pre-release identifiers to exclude from the candidate list.\n",[108,377,379],{"class":110,"line":378},36,[108,380,381],{},"  \"preReleaseFilter\": [\"-alpha\", \"-beta\", \"-rc\", \"-next\", \"-canary\", \"-dev\"],\n",[108,383,385],{"class":110,"line":384},37,[108,386,133],{"emptyLinePlaceholder":132},[108,388,390],{"class":110,"line":389},38,[108,391,392],{},"  // Restrict candidates to the same major as the currently declared version.\n",[108,394,396],{"class":110,"line":395},39,[108,397,398],{},"  \"sameMajor\": true\n",[108,400,402],{"class":110,"line":401},40,[108,403,404],{},"}\n",[10,406,407,408,411,412,415],{},"The ",[30,409,410],{},"lag"," value is the core of the policy. A lag of ",[30,413,414],{},"2"," means you're always targeting the second-most-recent stable release, in practice, a buffer of days to weeks depending on how actively a package is maintained. Push it higher for regulated or high-security environments where a months-long buffer makes more sense.",[10,417,418,421],{},[30,419,420],{},"minimumReleaseAge"," adds another layer on top: even if a version technically satisfies your lag count, it won't be selected as a target if it was published too recently.",[91,423,425],{"id":424},"ci-integration","CI integration",[10,427,428,430],{},[14,429,22],{}," also ships as a GitHub Action, so you can wire it into your pipeline with no additional setup:",[99,432,436],{"className":433,"code":434,"language":435,"meta":104,"style":104},"language-yaml shiki shiki-themes vitesse-black","- uses: actions/checkout@v4\n- uses: actions/setup-node@v4\n  with:\n    node-version: 20\n- uses: CRBroughton/recul@v1\n","yaml",[30,437,438,454,465,473,484],{"__ignoreMap":104},[108,439,440,444,448,451],{"class":110,"line":111},[108,441,443],{"class":442},"sK117","-",[108,445,447],{"class":446},"sm68I"," uses",[108,449,450],{"class":442},":",[108,452,453],{"class":125}," actions/checkout@v4\n",[108,455,456,458,460,462],{"class":110,"line":118},[108,457,443],{"class":442},[108,459,447],{"class":446},[108,461,450],{"class":442},[108,463,464],{"class":125}," actions/setup-node@v4\n",[108,466,467,470],{"class":110,"line":129},[108,468,469],{"class":446},"  with",[108,471,472],{"class":442},":\n",[108,474,475,478,480],{"class":110,"line":136},[108,476,477],{"class":446},"    node-version",[108,479,450],{"class":442},[108,481,483],{"class":482},"sxA9i"," 20\n",[108,485,486,488,490,492],{"class":110,"line":142},[108,487,443],{"class":442},[108,489,447],{"class":446},[108,491,450],{"class":442},[108,493,494],{"class":125}," CRBroughton/recul@v1\n",[10,496,497],{},"It writes a markdown summary directly to the Actions job summary automatically, a readable table per pipeline run, with no extra configuration required.",[91,499,501],{"id":500},"try-recul","Try Recul",[10,503,504,506,507,511],{},[14,505,22],{}," is open source under the MIT licence and available on ",[17,508,510],{"href":19,"rel":509},[21],"GitHub",".",[99,513,515],{"className":101,"code":514,"language":103,"meta":104,"style":104},"npm i -D @crbroughton/recul\n# or\npnpm add -D @crbroughton/recul\n",[30,516,517,532,537],{"__ignoreMap":104},[108,518,519,522,525,529],{"class":110,"line":111},[108,520,521],{"class":121},"npm",[108,523,524],{"class":125}," i",[108,526,528],{"class":527},"sXjYR"," -D",[108,530,531],{"class":125}," @crbroughton/recul\n",[108,533,534],{"class":110,"line":118},[108,535,536],{"class":114},"# or\n",[108,538,539,542,545,547],{"class":110,"line":129},[108,540,541],{"class":121},"pnpm",[108,543,544],{"class":125}," add",[108,546,528],{"class":527},[108,548,531],{"class":125},[550,551,552],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sux-A, html code.shiki .sux-A{--shiki-default:#758575DD}html pre.shiki code .sCK9x, html code.shiki .sCK9x{--shiki-default:#80A665}html pre.shiki code .s7rlk, html code.shiki .s7rlk{--shiki-default:#C98A7D}html pre.shiki code .sK117, html code.shiki .sK117{--shiki-default:#444444}html pre.shiki code .sm68I, html code.shiki .sm68I{--shiki-default:#B8A965}html pre.shiki code .sxA9i, html code.shiki .sxA9i{--shiki-default:#4C9A91}html pre.shiki code .sXjYR, html code.shiki .sXjYR{--shiki-default:#C99076}",{"title":104,"searchDepth":118,"depth":118,"links":554},[555,556,557,558,559],{"id":37,"depth":129,"text":38},{"id":93,"depth":118,"text":94},{"id":162,"depth":118,"text":163},{"id":424,"depth":118,"text":425},{"id":500,"depth":118,"text":501},"2026-05-07","Recently I released Recul, a CLI tool for npm and pnpm repositories that helps reduce supply chain risk by keeping your dependencies deliberately behind the latest release.","md",{},"/blog/reduce-supply-chain-risk-with-recul",{"title":5,"description":561},"blog/reduce-supply-chain-risk-with-recul",[568,569,570],"Security","Dependencies","Supply Chain","8pY84bR2sbitGYX0tAs7jE-Pb2R-kxmKrWW4bfvVb3I",1778185442788]