This metrics tool terrifies bad developers

Start free trial
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

This tutorial covers the SKILL.md format in detail, walks through best practices for skill design, and provides five production-ready skill templates that can be dropped into any project.

Compatibility notice: This tutorial describes a proposed SKILL.md convention for structuring Claude Code instructions. As of writing, the officially documented customization mechanism for Claude Code is CLAUDE.md. Verify current official support for SKILL.md files and .claude/skills/ directory discovery at https://docs.anthropic.com/en/docs/claude-code before adopting this pattern. Behavior may vary across Claude Code versions; confirm compatibility with your installed version by running claude --version. Treat all "Confirm/Verify" notes in this tutorial as covered by this notice unless a specific verification step differs.

Table of Contents

Prerequisites

  1. Claude Code CLI installed and accessible in your PATH. Verify with claude --version.
  2. Shell environment: bash/zsh on Linux/macOS. Windows users should use WSL or create files manually in their preferred editor.
  3. A Git repository initialized at your project root.
  4. Any text editor that supports Markdown.
  5. Run claude --help to check whether .claude/skills/ directory discovery and SKILL.md files are supported in your version. If the documented customization file is CLAUDE.md, adapt this tutorial's conventions accordingly.

Why Agent Skills Change How You Work with Claude Code

Claude Code ships as a general-purpose coding agent: it can edit files, run shell commands, and search codebases. But it carries no memory of a team's architectural decisions, naming conventions, testing philosophies, or deployment constraints. Every new session starts from a blank slate, forcing developers to re-explain context that should be permanent.

This tutorial describes a persistent Markdown-based instruction file pattern called SKILL.md that shapes Claude's behavior at the project, user, or global level. These files act as composable, project-scoped behavior profiles. They survive across sessions, live in version control, and travel with the codebase so entire engineering teams share the same directives.

This tutorial covers the SKILL.md format in detail, walks through best practices for skill design, and provides five production-ready skill templates that can be dropped into any project.

How Claude Agent Skills Work Under the Hood

The Skill Resolution Hierarchy

The following describes the intended resolution hierarchy for SKILL.md files.

Project-level skills live in .claude/skills/ at the repository root. These are team-shared directives: coding standards, framework conventions, architectural constraints that apply to everyone working on that codebase. You commit them alongside source code, so they travel with the project. (Confirm that .claude/ is not listed in your .gitignore; if it is, skills will not be shared via version control. See the gitignore section below for the correct exception syntax.)

User-level skills reside in ~/.claude/skills/ within your home directory. Store your personal preferences here: verbosity, explanation depth, formatting, default language choices. These should not override team agreements.

Global settings operate at the broadest scope, applying across all projects and sessions for a given Claude Code installation. The exact path and mechanism for global skills are not fully documented here; check claude --help or official documentation for a global config directory.

At session start, Claude loads all discoverable SKILL.md files and merges their instructions. If this hierarchy works as documented, project-level skills take precedence over user-level skills, which in turn override global settings. Test conflict resolution explicitly before relying on it. The result: a team's explicit conventions win over individual preferences, preventing one developer's personal style from overriding shared standards.

Anatomy of a SKILL.md File

A SKILL.md file uses natural-language Markdown with structured sections that Claude parses for behavioral directives. Anthropic has not confirmed whether a framework layer parses sections structurally or passes raw Markdown to the model; behavior may differ across versions. Some sections are essential for reliable skill activation; others add precision.

# Skill: [Descriptive Skill Name]

## Purpose
<!-- REQUIRED: One to three sentences defining what this skill does and when it applies. -->
<!-- Claude uses this to determine relevance to the current task. -->

## Trigger Conditions
<!-- OPTIONAL: Specify file types, task types, or keywords that activate this skill. -->
<!-- Example: "Activate when working with .ts files" or "Activate when asked to review code" -->

## Constraints
<!-- REQUIRED: Imperative directives defining what Claude must and must not do. -->
<!-- Use "Always..." and "Never..." for unambiguous rules. -->

## Output Format
<!-- RECOMMENDED: Define the structure, format, and style of Claude's responses. -->
<!-- Specify Markdown structure, code block language tags, section ordering. -->

## Examples
<!-- RECOMMENDED: Few-shot examples showing desired input/output pairs. -->
<!-- Include both positive examples and anti-patterns. -->

## Do This / Not This
<!-- OPTIONAL: Explicit comparison blocks for common mistake patterns. -->

## Notes
<!-- OPTIONAL: Edge cases, exceptions, or context Claude should weigh. -->

Note: HTML comments (<!-- ... -->) inside Markdown are valid but are not universally stripped by all tools. These comments may appear in Claude's context window as literal text. This is generally acceptable for instructional annotations but be aware of it when debugging unexpected behavior.

Claude processes the natural-language content of each section and treats imperatively worded statements as binding directives. Vague suggestions ("try to use TypeScript") produce inconsistent behavior; direct commands ("Always generate TypeScript interfaces before implementation code") produce reliable compliance.

Setting Up Your Skills Directory

Project-Level vs. User-Level Skills

Personal preferences are the simpler case: verbosity level, explanation depth, and formatting quirks go in ~/.claude/skills/. Anything that reflects a team agreement belongs in .claude/skills/ within the repository. Enforcing a specific testing framework, mandating error-handling patterns, requiring documentation formats: if the whole team should follow the rule, commit it to the project.

A clean directory convention uses one SKILL.md file per skill, with filenames that describe the skill's domain. The .SKILL.md double extension is a naming convention used in this tutorial for clarity; confirm whether Claude Code discovers files by path, by extension, or by another mechanism in the official documentation.

.claude/skills/
├── code-review.SKILL.md
├── api-design.SKILL.md
└── testing.SKILL.md

Creating Your First Skill File

The following commands create a project-level skills directory, initialize a minimal SKILL.md, and demonstrate how to verify that Claude Code recognizes it.

Platform note: The following commands are for bash/zsh (Linux/macOS). Windows users should use WSL or manually create the directory and file in their editor.

# Create the skills directory at the project root
# Guard: only run from repository root
git rev-parse --show-toplevel > /dev/null 2>&1 || { echo "Error: not in a git repository"; exit 1; }

mkdir -p .claude/skills/

# Create a minimal skill file — skips if file already exists to prevent overwrite
TARGET=".claude/skills/style-guide.SKILL.md"
if [ -f "$TARGET" ]; then
  echo "Skill file already exists at $TARGET — skipping to prevent overwrite."
  echo "Delete it manually before re-running if you intend to reset it."
else
  cat > "$TARGET" << 'EOF'
# Skill: Style Guide Enforcer

## Purpose
Enforce project coding style conventions when generating or modifying code.

## Trigger Conditions
Activate when generating, editing, or reviewing any source code file.

## Constraints
Always use single quotes for string literals in JavaScript and TypeScript.
Always add trailing commas in multi-line arrays, objects, and function parameters.
Never use default exports; always use named exports.

## Output Format
When suggesting changes, show the diff with before/after code blocks.
EOF
  echo "Skill file created at $TARGET"
fi

To verify skill loading, start a Claude Code session using your standard workflow, then ask: "What skills do you have loaded for this project?"

Note: Claude's self-reported skill inventory may not be fully accurate. Cross-validate by checking for directive compliance in actual task outputs, not only by asking Claude to enumerate its instructions. For example, ask Claude to generate a JavaScript file and verify that it uses single quotes and named exports as specified in the skill.

SKILL.md Format and Structure Best Practices

Writing Clear Behavioral Directives

The single biggest factor in skill compliance is directive specificity. Claude interprets imperative statements as rules and hedged language as optional guidance.

Weak: "Consider using error boundaries in React components."
Strong: "Always wrap route-level React components in an ErrorBoundary. Use the shared ErrorBoundary from src/components/ErrorBoundary.tsx."

Every constraint should name the specific technology, pattern, or file path it references. Scoping constraints precisely prevents Claude from over-applying a rule intended for one context across unrelated tasks.

Defining Trigger Conditions and Context Boundaries

Trigger conditions tell Claude when a skill is relevant. Without them, every skill loads into every prompt, creating noise and increasing the chance of conflicting directives.

Trigger conditions should reference observable context: file extensions (.ts, .sql, .md), task types ("when asked to write tests," "when generating API endpoints"), or explicit keywords ("when the user mentions 'migration'"). Overly broad triggers like "activate for all development tasks" defeat the purpose of scoping.

Context boundaries prevent interference. A database migration skill that specifies "Only apply these directives when generating or reviewing migration files. Do not apply to application code or test files." keeps Claude from injecting migration-specific patterns into unrelated work.

Including Examples and Anti-Patterns

Few-shot examples inside SKILL.md files produce more consistent output than abstract rules alone. When Claude sees a concrete input/output pair, it calibrates its behavior far more precisely.

Structure these as explicit "Do this / Not this" blocks:

## Do This / Not This

### Do This:
When defining an API error response, use the standard envelope:
{ "error": { "code": "VALIDATION_FAILED", "message": "Email is required", "field": "email" } }

### Not This:
{ "message": "Validation failed", "status": 400 }

Anti-patterns are especially powerful for preventing common mistakes that abstract rules fail to catch.

The Skill Manifest Format Guide

The following reference table documents every supported section in a SKILL.md file, its purpose, requirement level, and example content.

SectionPurposeRequired?Example Value
# Skill: [Name]Unique identifier for the skillYes# Skill: Code Review Guardian
## PurposeDefines what the skill does and its activation contextYes"Enforce security and performance standards during code review."
## Trigger ConditionsSpecifies when Claude should apply this skillNo (but recommended)"Activate when reviewing pull requests or when asked to review code."
## ConstraintsImperative behavioral rules (always/never directives)Yes"Always flag SQL queries built with string concatenation."
## Output FormatResponse structure, formatting, section orderingNo (but recommended)"Return findings as a Markdown table with columns: Severity, File, Line, Issue, Fix."
## ExamplesFew-shot input/output pairs demonstrating desired behaviorNo (but recommended)Concrete before/after code blocks showing correct application.
## Do This / Not ThisExplicit contrast pairs for common mistake patternsNoSide-by-side correct vs. incorrect implementations.
## NotesEdge cases, exceptions, environment-specific caveatsNo"Skip this rule for files in the /legacy directory."

This table serves as a quick reference when authoring new skills or auditing existing ones.

Below is a before/after comparison demonstrating the difference between a poorly structured skill and a properly structured one:

# BAD: Vague, unstructured skill
# Skill: Code Helper

Help write better code. Use good patterns. Make sure things are tested.
Try to follow best practices for whatever language we're using.
# GOOD: Specific, structured skill
# Skill: TypeScript Service Layer Generator

## Purpose
Generate service layer classes for TypeScript backend applications following
the repository pattern with dependency injection.

## Trigger Conditions
Activate when asked to create a new service, generate business logic,
or scaffold a module in a TypeScript backend project.

## Constraints
Always define an interface for each service before the implementation class.
Always inject dependencies through constructor parameters, never import directly.
Always return Result<T, E> types instead of throwing exceptions.
Import Result from the project's established result library (e.g., neverthrow,
fp-ts, or the internal result type defined in src/types/result.ts).
Never put database queries directly in service methods; delegate to repository classes.
Never use the `any` type; define explicit types or use `unknown` with type guards.

## Output Format
Generate files in this order: interface, implementation, barrel export.
Use JSDoc comments on every public method with @param and @returns tags.
Always include the full import path for injected interfaces in generated constructor code.

## Do This / Not This
### Do This:
import { IUserRepository } from './interfaces/user-repository.interface';
constructor(private readonly userRepo: IUserRepository) {}

### Not This:
import { userRepository } from './user.repository';

5 Ready-to-Use Claude Agent Skill Templates

These templates are designed for direct use in .claude/skills/. Each follows the structured format covered above, with purpose statements, scoped constraints, output formatting rules, and examples where they add clarity. Customize the framework and language references to match your stack.

Skill 1: Code Review Guardian

# Skill: Code Review Guardian

## Purpose
Perform thorough code reviews that enforce security, performance, and
maintainability standards. Classify all findings by severity.

## Trigger Conditions
Activate when asked to review code, audit a pull request, or assess
code quality of any source file.

## Constraints
Always classify each finding as CRITICAL, WARNING, or SUGGESTION.
Always check for: SQL injection, XSS vectors, hardcoded secrets,
unvalidated input, missing error handling, and N+1 query patterns.
Always verify that public functions have input validation.
Never approve code with CRITICAL findings; list all required fixes.
Never combine security issues with style issues in the same finding.
Distinguish clearly between "Required Fix" and "Optional Suggestion."

## Output Format
Return findings as a Markdown table:
| Severity | File:Line | Issue | Recommended Fix |
Follow the table with a summary: total findings by severity, overall
assessment (APPROVE, REQUEST_CHANGES, or BLOCK).

## Do This / Not This
### Do This:
For password verification, use a constant-time comparison via a
dedicated password hashing library (e.g., bcrypt.compare, argon2.verify).
If using crypto.timingSafeEqual directly, both arguments MUST be
Buffer instances of IDENTICAL byte length:

```ts
const a = Buffer.from(hmac1, 'hex');
const b = Buffer.from(hmac2, 'hex');
if (a.length !== b.length) return false; // length leak is acceptable for HMAC comparison
return crypto.timingSafeEqual(a, b);
```

Never call crypto.timingSafeEqual on raw strings or Buffers of different
lengths — this throws TypeError at runtime.

| CRITICAL | auth.ts:42 | Password compared with == instead of timing-safe comparison | Use crypto.timingSafeEqual() with equal-length Buffers, or use bcrypt.compare / argon2.verify for password hashing |

### Not This:
"There might be some security issues in the auth file."

crypto.timingSafeEqual(passwordInput, storedPassword)
// Throws if lengths differ; does not hash — wrong usage

## Notes
For legacy files in /vendor or /third_party, flag issues but mark as
"Legacy — track in tech debt backlog" instead of blocking.
Claude is not a substitute for automated secret scanning tools. Use
dedicated secret scanning tooling in CI/CD pipelines (configure according
to your project's CI setup).

Skill 2: API Endpoint Architect

# Skill: API Endpoint Architect

## Purpose
Generate REST API endpoints following consistent naming, authentication,
error handling, and response envelope conventions.

## Trigger Conditions
Activate when asked to create, design, or scaffold API endpoints,
routes, or controllers.

## Constraints
Always use plural nouns for resource names in URLs (/users, not /user).
Always version APIs in the URL path (/v1/users).
Always wrap responses in the standard envelope:
{
  "data": ...,
  "meta": {
    "requestId": "<uuid-v4-generated-at-request-ingress>",
    "timestamp": "<ISO-8601-UTC-timestamp>"
  }
}
Always wrap errors in:
{
  "error": {
    "code": "<SCREAMING_SNAKE_CASE_error_code>",
    "message": "<user-safe message>",
    "details": ["<user-safe field or validation message only>"]
  }
}
Always generate requestId using a UUID v4 library at request ingress middleware.
Never initialize requestId or timestamp to empty strings.
Always require authentication by default; explicitly mark public endpoints.
Never return stack traces or internal error details in production responses.
Never populate the error details array with stack traces, internal exception
messages, database error text, internal service names, hostnames, file paths,
or raw validation library output without sanitization. Only include: field
names that failed validation, user-readable constraint descriptions, and
safe enumeration of accepted values.
Never use verbs in URL paths; use HTTP methods to indicate actions.
Always set explicit timeouts on all external calls (HTTP, database, queues).
Never leave network or database calls without a timeout parameter.
Map HTTP status codes consistently: 200 success, 201 created, 400 validation,
401 unauthenticated, 403 forbidden, 404 not found, 409 conflict, 500 server error.

## Output Format
For each endpoint, generate: route definition, request validation schema,
controller method, and example curl command.
Group related endpoints under a resource heading.

## Notes
For GraphQL projects, adapt naming conventions to query/mutation patterns
but maintain the same error envelope structure.
The "Never return stack traces in production" directive should also be
enforced at the infrastructure and framework level, not solely through
Claude-generated code.

Skill 3: Test Suite Generator

# Skill: Test Suite Generator

## Purpose
Generate comprehensive test suites covering unit, integration, and edge
case scenarios for the specified code under test.

## Trigger Conditions
Activate when asked to write tests, generate test cases, or improve
test coverage for any source file or module.

## Constraints
Always generate at minimum: 3 happy-path tests, 2 error/failure tests,
and 2 edge-case tests per public function or method. (This is a recommended
convention; adjust the ratio to your project's quality requirements.)
Always use the Arrange-Act-Assert pattern with labeled comment sections.
Always mock external dependencies (databases, APIs, file system) in unit tests.
Always mock HTTP clients at the module boundary using the project's established
mocking library (e.g., jest.mock for Jest, msw for fetch-based code, nock for
the http module, unittest.mock for Python). Never stub only the return value
of an already-instantiated client.
Never make real network calls in unit tests.
Never test private methods directly; test through public interfaces.
Always include a test for null/undefined input on every function that
accepts reference types.
Always set a per-test timeout appropriate to the operation under test.
Use descriptive test names following: "should [expected behavior] when [condition]."

## Output Format
Group tests by function under a describe/context block.
Place mock setup in beforeEach blocks when shared across tests.
Add a coverage summary comment at the top listing which scenarios are covered.

## Do This / Not This
### Do This:
it('should return 404 when user ID does not exist in database', async () => {

### Not This:
it('test error case', () => {

## Notes
Adapt framework syntax to the project's existing test framework (Jest,
Vitest, Mocha, pytest, etc.) based on config files present in the repo.

Skill 4: Database Migration Planner

# Skill: Database Migration Planner

## Purpose
Plan and generate safe database migrations with rollback strategies,
ensuring backward compatibility and data preservation.

## Trigger Conditions
Activate when asked to create database migrations, alter schemas,
add or remove columns, or modify indexes.

## Constraints
Always generate both an "up" and "down" migration for every change.
Always make migrations backward compatible with the previous release.
Never drop columns directly; use a three-phase approach: deprecate,
stop reading, then drop in a subsequent release.
Always add indexes concurrently on PostgreSQL using CREATE INDEX CONCURRENTLY.
CRITICAL: CREATE INDEX CONCURRENTLY must be placed in a DEDICATED migration
file that runs outside any transaction block. Never combine it with other
DDL statements in the same migration file. Configure the migration framework
to disable automatic transaction wrapping for this specific file
(e.g., Knex: knex.raw with disableTransactions, Alembic: op.execute with
execute_timeout outside transaction, Rails: disable_ddl_transaction!).
Failure to do this will raise:
ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block.
Never write data-destructive migrations without an explicit data backup step.
Always include a comment block at the top with: migration purpose,
affected tables, estimated row count impact, and rollback risk level.
Name migration files using a timestamp prefix generated by the shell:
  $(date +%Y%m%d%H%M%S)_<snake_case_description>.<ext>
Examples:
  20240115143022_add_index_to_users_email.sql
  20240115143022_add_index_to_users_email.ts  (for TypeScript ORM frameworks)
Never use the literal string YYYYMMDDHHMMSS as a filename prefix.
Always verify the generated filename sorts correctly among existing
migration files before committing.

## Output Format
Generate the migration file with SQL or ORM-specific syntax matching
the project's migration framework (Knex, Prisma, Alembic, ActiveRecord).
Follow the migration with a rollback verification checklist.

## Notes
For tables exceeding 10 million rows, flag the migration as requiring
off-peak execution and include an estimated lock duration warning.

Skill 5: Documentation Writer

# Skill: Documentation Writer

## Purpose
Produce technical documentation that matches the project's existing
voice, structure, section conventions, and audience level.

## Trigger Conditions
Activate when asked to write, update, or review documentation,
READMEs, API docs, or architectural decision records.

## Constraints
Always match the heading hierarchy and section order of existing docs.
Always include a one-sentence summary at the top of every document.
Always provide at least one working code example per documented function.
Never use jargon without defining it on first use.
Never write documentation that references internal implementation
details unless the audience is explicitly developers on the team.
Always include a "Prerequisites" section for any setup or tutorial doc.
Cross-reference related docs with relative Markdown links, not absolute URLs.

## Output Format
Use hash-prefixed headings (`#`, `##`, `###`) rather than underline-style headings.
Wrap code examples in fenced blocks with language tags.
Add a "Last Updated" line at the document footer.

## Notes
For API reference docs, follow the structure: description, parameters
table, response example, error codes, and related endpoints.

Advanced Skill Composition and Management

Composing Multiple Skills for Complex Workflows

When multiple SKILL.md files are active simultaneously, Claude merges their directives. In cases of direct contradiction, Claude resolves conflicts using the scope hierarchy (project over user over global). Within the same scope, conflict resolution behavior is not formally guaranteed. Avoid overlapping directives at the same scope level; if overlap is unavoidable, test empirically across multiple prompts to confirm consistent behavior.

To avoid repeating shared rules in every skill file, layer a base skill with specialization skills. A base typescript-conventions.SKILL.md defines general TypeScript rules. A specialized react-component.SKILL.md adds React-specific directives and references the base skill: "In addition to the TypeScript Conventions skill, apply these constraints when generating React components." This cross-reference is a natural-language hint to the model, not a programmatic import; Claude does not resolve skill names to files. This approach keeps individual files focused and maintainable.

Naming conventions help indicate relationships: prefix shared base skills with 00- and specializations with higher numbers (01-react, 02-api) to signal intended loading order. Note that this is a human-readability convention only; guaranteed file processing order by Claude is not confirmed.

Versioning and Sharing Skills Across Teams

Since .claude/skills/ lives at the project root, it commits to version control alongside application code, provided .claude/ is not in your .gitignore. If .claude/ is already ignored by a parent rule in your .gitignore, you must un-ignore both the directory and the target files. Order matters in .gitignore:

# If .claude/ is already ignored by a parent rule, you must un-ignore
# both the directory AND the target files. Order matters.

# Step 1: Un-ignore the parent directory
!.claude/

# Step 2: Un-ignore the skills subdirectory
!.claude/skills/

# Step 3: Un-ignore skill files (adjust glob if using different extension)
!.claude/skills/*.SKILL.md
!.claude/skills/*.md

# Verify with: git check-ignore -v .claude/skills/style-guide.SKILL.md
# Expected output: no output (file is not ignored)

Simply adding !.claude/skills/ alone will not work if .claude/ is already ignored. Git does not un-ignore files inside an ignored directory without also un-ignoring the directory itself.

This means skill changes follow the same pull request review process as any other code change. Teams benefit from treating skills as living artifacts: reviewing proposed directive changes, testing them against representative prompts before merging, and maintaining a changelog.

Larger organizations can maintain a central skill registry repository that individual projects pull from, ensuring baseline standards propagate across all codebases while allowing project-specific overrides.

Debugging Skill Conflicts

When Claude appears to ignore a directive, start by asking Claude to enumerate its loaded skills. But treat that as a starting point only; Claude's self-reported context is not always accurate. Validate by observing directive compliance in actual task outputs. If a style skill mandates single quotes, generate a code file and inspect the output rather than relying solely on Claude's self-description.

If the skill is loaded but a specific directive is not followed, the issue is usually ambiguity in the directive's wording or a conflicting directive from another skill.

Isolating skills by temporarily moving all but one out of the directory helps identify which file introduces the conflict.

Common Mistakes and How to Avoid Them

A skill that tries to cover code review, testing, and documentation in one file will produce diluted, inconsistent behavior. Split broad skills into focused files with precise trigger conditions.

Specifying what Claude should do without specifying what it should never do leaves gaps. Unwanted patterns creep in. Never directives are effective for preventing specific, persistent anti-patterns that positive rules alone do not catch.

If you omit output format specifications, Claude picks its own formatting, which varies between sessions. Explicitly define section order, heading style, and response structure to eliminate that inconsistency.

"Consider adding error handling" gets ignored. "Always wrap async operations in try/catch with typed error responses" does not. Write directives as commands, not suggestions. Claude follows them unreliably when they read as optional guidance.

Committing a skill without testing it against diverse inputs means discovering failures during real work. Test against several prompts that span the skill's intended scope and its boundary conditions: prompts that should activate the skill, and prompts that should not.

Building Your Skill Library

SKILL.md files give you persistent control over Claude Code's behavior. They transform a general-purpose agent into a specialized collaborator that understands a team's specific standards, patterns, and constraints without re-explanation every session.

The most effective approach is iterative: start with one template from this article, customize it against a real project's needs, and refine the directives as edge cases surface during actual use.

Anthropic's official Claude Code documentation provides ongoing updates. See: https://docs.anthropic.com/en/docs/claude-code -- verify the SKILL.md format is documented there before building a production skill library.

SitePoint TeamSitePoint Team

Sharing our passion for building incredible internet things.

© 2000 – 2026 SitePoint Pty. Ltd.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.