Skip to content
Back to skills

2586 Dev Endpoint Ad8342d7

ASecurity

Implementation patterns for a development-only HTTP endpoint that agents can query for log entries.

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 31, 2026
developmentrubynextjsnodeexpressrailsapisecurity

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned May 31, 2026

npx -y skills add tools-only/X-Skills --skill 2518-dev-endpoint_ad8342d7 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 2586 Dev Endpoint Ad8342d7?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 2586 Dev Endpoint Ad8342d7
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-2586-dev-endpoint-ad8342d7/badge)](https://www.skillsdirectory.com/skills/tools-only-2586-dev-endpoint-ad8342d7)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Dev Log Endpoint

Implementation patterns for a development-only HTTP endpoint that agents can query for log entries.

## Table of Contents

- [Next.js App Router](#nextjs-app-router)
- [Express](#express)
- [Rails](#rails)
- [Security](#security)

## Next.js App Router

```ts
// app/api/__dev/logs/route.ts
import { NextRequest, NextResponse } from "next/server";
import { readFileSync } from "fs";

// Only available in development
if (process.env.NODE_ENV !== "development") {
  throw new Error("Dev log endpoint must not be loaded in production");
}

const LOG_FILE = process.env.LOG_FILE || "logs/app.json";
const MAX_ENTRIES = 500;

export async function GET(req: NextRequest) {
  const params = req.nextUrl.searchParams;
  const level = params.get("level");
  const path = params.get("path");
  const requestId = params.get("requestId");
  const last = Math.min(parseInt(params.get("last") || "100"), MAX_ENTRIES);
  const since = params.get("since"); // ISO timestamp

  let lines: string[];
  try {
    lines = readFileSync(LOG_FILE, "utf-8").trim().split("\n");
  } catch {
    return NextResponse.json({ error: "Log file not found", path: LOG_FILE }, { status: 404 });
  }

  let entries = lines
    .map((line) => { try { return JSON.parse(line); } catch { return null; } })
    .filter(Boolean);

  if (level) entries = entries.filter((e) => e.level === level);
  if (path) entries = entries.filter((e) => e.path?.includes(path));
  if (requestId) entries = entries.filter((e) => e.requestId === requestId);
  if (since) entries = entries.filter((e) => new Date(e.timestamp) >= new Date(since));

  entries = entries.slice(-last);

  return NextResponse.json(entries);
}
```

## Express

```ts
// routes/dev-logs.ts
import { Router, Request, Response } from "express";
import { readFileSync } from "fs";

const router = Router();
const LOG_FILE = process.env.LOG_FILE || "logs/app.json";
const MAX_ENTRIES = 500;

router.get("/__dev/logs", (req: Request, res: Response) => {
  const { level, path, requestId, last = "100", since } = req.query;
  const limit = Math.min(parseInt(last as string), MAX_ENTRIES);

  let lines: string[];
  try {
    lines = readFileSync(LOG_FILE, "utf-8").trim().split("\n");
  } catch {
    return res.status(404).json({ error: "Log file not found", path: LOG_FILE });
  }

  let entries = lines
    .map((line) => { try { return JSON.parse(line); } catch { return null; } })
    .filter(Boolean);

  if (level) entries = entries.filter((e) => e.level === level);
  if (path) entries = entries.filter((e) => e.path?.includes(path as string));
  if (requestId) entries = entries.filter((e) => e.requestId === requestId);
  if (since) entries = entries.filter((e) => new Date(e.timestamp) >= new Date(since as string));

  res.json(entries.slice(-limit));
});

export default router;

// app.ts — only register in development
if (process.env.NODE_ENV === "development") {
  app.use(devLogsRouter);
}
```

## Rails

```ruby
# config/routes.rb
if Rails.env.development?
  get '__dev/logs', to: 'dev/logs#index'
end

# app/controllers/dev/logs_controller.rb
module Dev
  class LogsController < ApplicationController
    skip_before_action :authenticate_user!, raise: false
    MAX_ENTRIES = 500

    def index
      log_path = Rails.root.join('log', 'structured.json')
      unless File.exist?(log_path)
        return render json: { error: 'Log file not found', path: log_path.to_s }, status: :not_found
      end

      lines = File.readlines(log_path).last([params.fetch(:last, 100).to_i, MAX_ENTRIES].min)
      entries = lines.filter_map { |line| JSON.parse(line) rescue nil }

      entries = entries.select { |e| e['level'] == params[:level] } if params[:level]
      entries = entries.select { |e| e['path']&.include?(params[:path]) } if params[:path]
      entries = entries.select { |e| e['request_id'] == params[:request_id] } if params[:request_id]

      if params[:since]
        cutoff = Time.parse(params[:since])
        entries = entries.select { |e| Time.parse(e['timestamp']) >= cutoff }
      end

      render json: entries
    end
  end
end
```

## Security

**The dev endpoint MUST:**
- Be disabled in production (compile-time or startup check, not runtime)
- Not require authentication (agents need unauthenticated access in dev)
- Not expose sensitive fields (redact tokens, passwords, PII before logging)
- Limit response size to prevent memory issues

**Recommended approach — fail loudly if misconfigured:**

```ts
// Startup check, not runtime check
if (process.env.NODE_ENV === "production") {
  throw new Error("FATAL: Dev log endpoint loaded in production");
}
```

This is safer than `if (env !== "development") return 403` because it prevents the route from existing at all in production.

Files in this skill

  • README.md903 B
  • skill.md4.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.